From 60e7b84bc71763f8923f3f790f4e61fd5f60c924 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Tue, 30 Jun 2026 11:04:05 +1000 Subject: [PATCH 1/2] Fix issues where code diverged from BCL behavior Each polyfill below produced an observably different result from the real .NET API it emulates; fixed to match the BCL, with a regression test per bug that runs on both the polyfill (older TFMs) and the BCL (newer TFMs). Encoding / strings: - Encoding.GetChars(ROS, Span): copy only the decoded char count instead of the whole scratch buffer (threw/clobbered on non-ASCII) - string.ReplaceLineEndings: handle empty input, preserve a trailing line ending, and recognise the full BCL set (FF/NEL/LS/PS) - string.Join(char, ROS): treat null elements as empty (unsafe path) - StringBuilder.Append(sb,int,int)/CopyTo: validate arguments Date / time: - DateTime/DateTimeOffset.AddMicroseconds: preserve sub-millisecond precision via AddTicks (AddMilliseconds rounds on .NET Framework) - DateTime/DateTimeOffset/TimeSpan Microsecond & Nanosecond: correct the TicksPerMicrosecond constant (10, not 10,000,000) and the formula - TimeSpan.FromDays/Hours/Minutes/Seconds/Milliseconds/Microseconds: throw ArgumentOutOfRangeException on overflow, accumulating in microseconds (decimal) so cancelling components stay in range Collections / spans: - Span.Sort(keys, values, Comparison): pass null keys to the comparison - ConcurrentDictionary.GetOrAdd: validate valueFactory - List.CopyTo(Span)/InsertRange: validate destination length / index - EqualityComparer.Create: validate the delegate Numbers / Convert: - double/float.TryParse(.., provider, ..): include NumberStyles.AllowThousands (the BCL default) in the default-style overloads - Convert.FromHexString (both OperationStatus overloads): correct charsConsumed on invalid data and give DestinationTooSmall priority - ArgumentOutOfRangeException.ThrowIfZero: set ActualValue and message; fix two nint relational message strings Reflection: - Type.GetMethod(name, genericParameterCount, ..): argument validation - MemberInfo.HasSameMetadataDefinitionAs(null): throw ArgumentNullException IO / XML / compression / crypto / async: - File.GetUnixFileMode/SetUnixFileMode: correct setuid(4)/setgid(2) mapping - XDocument/XElement.LoadAsync(Stream): honor the XML-declared encoding instead of forcing UTF-8 - ZipFile.ExtractToDirectoryAsync(overwriteFiles:true): overwrite per file instead of deleting the whole destination directory - RandomNumberGenerator.GetInt32: off-by-one rejection that never returned the maximum value - TaskCompletionSource.TrySetCanceled(token): forward the token - ZLibStream: document that the Adler-32 trailer is not validated --- assemblySize.include.md | 78 ++--- readme.md | 78 ++--- .../ArgumentOutOfRangeExceptionPolyfill.cs | 14 +- src/Polyfill/ConvertPolyfill.cs | 34 +- src/Polyfill/DateTimeOffsetPolyfill.cs | 2 +- src/Polyfill/DateTimePolyfill.cs | 2 +- src/Polyfill/EqualityComparerPolyfill.cs | 22 +- src/Polyfill/FileUnixModePolyfill.cs | 8 +- src/Polyfill/Numbers/DoublePolyfill.cs | 8 +- src/Polyfill/Numbers/SinglePolyfill.cs | 8 +- src/Polyfill/Polyfill.cs | 2 +- src/Polyfill/Polyfill_ArraySegment.cs | 13 +- src/Polyfill/Polyfill_ConcurrentDictionary.cs | 5 + src/Polyfill/Polyfill_Encoding_GetChars.cs | 2 +- src/Polyfill/Polyfill_List.cs | 10 + src/Polyfill/Polyfill_Memory_SpanSort.cs | 12 +- src/Polyfill/Polyfill_MicroNanosecondAdd.cs | 8 +- src/Polyfill/Polyfill_String.cs | 48 ++- src/Polyfill/Polyfill_StringBuilder_Append.cs | 10 + src/Polyfill/Polyfill_StringBuilder_CopyTo.cs | 37 ++- src/Polyfill/Polyfill_Type.cs | 36 ++- src/Polyfill/RandomNumberGeneratorPolyfill.cs | 2 +- src/Polyfill/StringPolyfill.cs | 5 + src/Polyfill/TaskCompletionSource.cs | 2 +- src/Polyfill/TimeSpanPolyfill.cs | 70 ++-- src/Polyfill/XDocumentPolyfill.cs | 8 +- src/Polyfill/XElementPolyfill.cs | 8 +- src/Polyfill/ZLibStream.cs | 4 + src/Polyfill/ZipFilePolyfill.cs | 60 +++- src/Split/net10.0/EqualityComparerPolyfill.cs | 10 +- src/Split/net10.0/Polyfill.cs | 2 +- src/Split/net10.0/Polyfill_String.cs | 2 - src/Split/net11.0/Polyfill.cs | 2 +- .../ArgumentOutOfRangeExceptionPolyfill.cs | 12 +- src/Split/net461/ConvertPolyfill.cs | 28 +- src/Split/net461/DateTimeOffsetPolyfill.cs | 2 +- src/Split/net461/DateTimePolyfill.cs | 2 +- src/Split/net461/EqualityComparerPolyfill.cs | 20 +- src/Split/net461/FileUnixModePolyfill.cs | 8 +- src/Split/net461/Numbers/DoublePolyfill.cs | 8 +- src/Split/net461/Numbers/SinglePolyfill.cs | 8 +- src/Split/net461/Polyfill.cs | 2 +- src/Split/net461/Polyfill_ArraySegment.cs | 12 +- .../net461/Polyfill_ConcurrentDictionary.cs | 4 + .../net461/Polyfill_Encoding_GetChars.cs | 2 +- src/Split/net461/Polyfill_List.cs | 8 + src/Split/net461/Polyfill_Memory_SpanSort.cs | 8 +- .../net461/Polyfill_MicroNanosecondAdd.cs | 4 +- src/Split/net461/Polyfill_String.cs | 43 ++- .../net461/Polyfill_StringBuilder_Append.cs | 8 + .../net461/Polyfill_StringBuilder_CopyTo.cs | 31 +- src/Split/net461/Polyfill_Type.cs | 31 +- .../net461/RandomNumberGeneratorPolyfill.cs | 2 +- src/Split/net461/StringPolyfill.cs | 4 + src/Split/net461/TaskCompletionSource.cs | 2 +- src/Split/net461/TimeSpanPolyfill.cs | 63 ++-- src/Split/net461/XDocumentPolyfill.cs | 6 +- src/Split/net461/XElementPolyfill.cs | 6 +- src/Split/net461/ZipFilePolyfill.cs | 45 ++- .../ArgumentOutOfRangeExceptionPolyfill.cs | 12 +- src/Split/net462/ConvertPolyfill.cs | 28 +- src/Split/net462/DateTimeOffsetPolyfill.cs | 2 +- src/Split/net462/DateTimePolyfill.cs | 2 +- src/Split/net462/EqualityComparerPolyfill.cs | 20 +- src/Split/net462/FileUnixModePolyfill.cs | 8 +- src/Split/net462/Numbers/DoublePolyfill.cs | 8 +- src/Split/net462/Numbers/SinglePolyfill.cs | 8 +- src/Split/net462/Polyfill.cs | 2 +- src/Split/net462/Polyfill_ArraySegment.cs | 12 +- .../net462/Polyfill_ConcurrentDictionary.cs | 4 + .../net462/Polyfill_Encoding_GetChars.cs | 2 +- src/Split/net462/Polyfill_List.cs | 8 + src/Split/net462/Polyfill_Memory_SpanSort.cs | 8 +- .../net462/Polyfill_MicroNanosecondAdd.cs | 4 +- src/Split/net462/Polyfill_String.cs | 43 ++- .../net462/Polyfill_StringBuilder_Append.cs | 8 + .../net462/Polyfill_StringBuilder_CopyTo.cs | 31 +- src/Split/net462/Polyfill_Type.cs | 31 +- .../net462/RandomNumberGeneratorPolyfill.cs | 2 +- src/Split/net462/StringPolyfill.cs | 4 + src/Split/net462/TaskCompletionSource.cs | 2 +- src/Split/net462/TimeSpanPolyfill.cs | 63 ++-- src/Split/net462/XDocumentPolyfill.cs | 6 +- src/Split/net462/XElementPolyfill.cs | 6 +- src/Split/net462/ZipFilePolyfill.cs | 45 ++- .../ArgumentOutOfRangeExceptionPolyfill.cs | 12 +- src/Split/net47/ConvertPolyfill.cs | 28 +- src/Split/net47/DateTimeOffsetPolyfill.cs | 2 +- src/Split/net47/DateTimePolyfill.cs | 2 +- src/Split/net47/EqualityComparerPolyfill.cs | 20 +- src/Split/net47/FileUnixModePolyfill.cs | 8 +- src/Split/net47/Numbers/DoublePolyfill.cs | 8 +- src/Split/net47/Numbers/SinglePolyfill.cs | 8 +- src/Split/net47/Polyfill.cs | 2 +- src/Split/net47/Polyfill_ArraySegment.cs | 12 +- .../net47/Polyfill_ConcurrentDictionary.cs | 4 + src/Split/net47/Polyfill_Encoding_GetChars.cs | 2 +- src/Split/net47/Polyfill_List.cs | 8 + src/Split/net47/Polyfill_Memory_SpanSort.cs | 8 +- .../net47/Polyfill_MicroNanosecondAdd.cs | 4 +- src/Split/net47/Polyfill_String.cs | 43 ++- .../net47/Polyfill_StringBuilder_Append.cs | 8 + .../net47/Polyfill_StringBuilder_CopyTo.cs | 31 +- src/Split/net47/Polyfill_Type.cs | 31 +- .../net47/RandomNumberGeneratorPolyfill.cs | 2 +- src/Split/net47/StringPolyfill.cs | 4 + src/Split/net47/TaskCompletionSource.cs | 2 +- src/Split/net47/TimeSpanPolyfill.cs | 63 ++-- src/Split/net47/XDocumentPolyfill.cs | 6 +- src/Split/net47/XElementPolyfill.cs | 6 +- src/Split/net47/ZipFilePolyfill.cs | 45 ++- .../ArgumentOutOfRangeExceptionPolyfill.cs | 12 +- src/Split/net471/ConvertPolyfill.cs | 28 +- src/Split/net471/DateTimeOffsetPolyfill.cs | 2 +- src/Split/net471/DateTimePolyfill.cs | 2 +- src/Split/net471/EqualityComparerPolyfill.cs | 20 +- src/Split/net471/FileUnixModePolyfill.cs | 8 +- src/Split/net471/Numbers/DoublePolyfill.cs | 8 +- src/Split/net471/Numbers/SinglePolyfill.cs | 8 +- src/Split/net471/Polyfill.cs | 2 +- src/Split/net471/Polyfill_ArraySegment.cs | 12 +- .../net471/Polyfill_ConcurrentDictionary.cs | 4 + .../net471/Polyfill_Encoding_GetChars.cs | 2 +- src/Split/net471/Polyfill_List.cs | 8 + src/Split/net471/Polyfill_Memory_SpanSort.cs | 8 +- .../net471/Polyfill_MicroNanosecondAdd.cs | 4 +- src/Split/net471/Polyfill_String.cs | 43 ++- .../net471/Polyfill_StringBuilder_Append.cs | 8 + .../net471/Polyfill_StringBuilder_CopyTo.cs | 31 +- src/Split/net471/Polyfill_Type.cs | 31 +- .../net471/RandomNumberGeneratorPolyfill.cs | 2 +- src/Split/net471/StringPolyfill.cs | 4 + src/Split/net471/TaskCompletionSource.cs | 2 +- src/Split/net471/TimeSpanPolyfill.cs | 63 ++-- src/Split/net471/XDocumentPolyfill.cs | 6 +- src/Split/net471/XElementPolyfill.cs | 6 +- src/Split/net471/ZipFilePolyfill.cs | 45 ++- .../ArgumentOutOfRangeExceptionPolyfill.cs | 12 +- src/Split/net472/ConvertPolyfill.cs | 28 +- src/Split/net472/DateTimeOffsetPolyfill.cs | 2 +- src/Split/net472/DateTimePolyfill.cs | 2 +- src/Split/net472/EqualityComparerPolyfill.cs | 20 +- src/Split/net472/FileUnixModePolyfill.cs | 8 +- src/Split/net472/Numbers/DoublePolyfill.cs | 8 +- src/Split/net472/Numbers/SinglePolyfill.cs | 8 +- src/Split/net472/Polyfill.cs | 2 +- src/Split/net472/Polyfill_ArraySegment.cs | 12 +- .../net472/Polyfill_Encoding_GetChars.cs | 2 +- src/Split/net472/Polyfill_List.cs | 8 + src/Split/net472/Polyfill_Memory_SpanSort.cs | 8 +- .../net472/Polyfill_MicroNanosecondAdd.cs | 4 +- src/Split/net472/Polyfill_String.cs | 43 ++- .../net472/Polyfill_StringBuilder_Append.cs | 8 + .../net472/Polyfill_StringBuilder_CopyTo.cs | 31 +- src/Split/net472/Polyfill_Type.cs | 31 +- .../net472/RandomNumberGeneratorPolyfill.cs | 2 +- src/Split/net472/StringPolyfill.cs | 4 + src/Split/net472/TaskCompletionSource.cs | 2 +- src/Split/net472/TimeSpanPolyfill.cs | 63 ++-- src/Split/net472/XDocumentPolyfill.cs | 6 +- src/Split/net472/XElementPolyfill.cs | 6 +- src/Split/net472/ZipFilePolyfill.cs | 45 ++- .../ArgumentOutOfRangeExceptionPolyfill.cs | 12 +- src/Split/net48/ConvertPolyfill.cs | 28 +- src/Split/net48/DateTimeOffsetPolyfill.cs | 2 +- src/Split/net48/DateTimePolyfill.cs | 2 +- src/Split/net48/EqualityComparerPolyfill.cs | 20 +- src/Split/net48/FileUnixModePolyfill.cs | 8 +- src/Split/net48/Numbers/DoublePolyfill.cs | 8 +- src/Split/net48/Numbers/SinglePolyfill.cs | 8 +- src/Split/net48/Polyfill.cs | 2 +- src/Split/net48/Polyfill_ArraySegment.cs | 12 +- src/Split/net48/Polyfill_Encoding_GetChars.cs | 2 +- src/Split/net48/Polyfill_List.cs | 8 + src/Split/net48/Polyfill_Memory_SpanSort.cs | 8 +- .../net48/Polyfill_MicroNanosecondAdd.cs | 4 +- src/Split/net48/Polyfill_String.cs | 43 ++- .../net48/Polyfill_StringBuilder_Append.cs | 8 + .../net48/Polyfill_StringBuilder_CopyTo.cs | 31 +- src/Split/net48/Polyfill_Type.cs | 31 +- .../net48/RandomNumberGeneratorPolyfill.cs | 2 +- src/Split/net48/StringPolyfill.cs | 4 + src/Split/net48/TaskCompletionSource.cs | 2 +- src/Split/net48/TimeSpanPolyfill.cs | 63 ++-- src/Split/net48/XDocumentPolyfill.cs | 6 +- src/Split/net48/XElementPolyfill.cs | 6 +- src/Split/net48/ZipFilePolyfill.cs | 45 ++- .../ArgumentOutOfRangeExceptionPolyfill.cs | 12 +- src/Split/net481/ConvertPolyfill.cs | 28 +- src/Split/net481/DateTimeOffsetPolyfill.cs | 2 +- src/Split/net481/DateTimePolyfill.cs | 2 +- src/Split/net481/EqualityComparerPolyfill.cs | 20 +- src/Split/net481/FileUnixModePolyfill.cs | 8 +- src/Split/net481/Numbers/DoublePolyfill.cs | 8 +- src/Split/net481/Numbers/SinglePolyfill.cs | 8 +- src/Split/net481/Polyfill.cs | 2 +- src/Split/net481/Polyfill_ArraySegment.cs | 12 +- .../net481/Polyfill_Encoding_GetChars.cs | 2 +- src/Split/net481/Polyfill_List.cs | 8 + src/Split/net481/Polyfill_Memory_SpanSort.cs | 8 +- .../net481/Polyfill_MicroNanosecondAdd.cs | 4 +- src/Split/net481/Polyfill_String.cs | 43 ++- .../net481/Polyfill_StringBuilder_Append.cs | 8 + .../net481/Polyfill_StringBuilder_CopyTo.cs | 31 +- src/Split/net481/Polyfill_Type.cs | 31 +- .../net481/RandomNumberGeneratorPolyfill.cs | 2 +- src/Split/net481/StringPolyfill.cs | 4 + src/Split/net481/TaskCompletionSource.cs | 2 +- src/Split/net481/TimeSpanPolyfill.cs | 63 ++-- src/Split/net481/XDocumentPolyfill.cs | 6 +- src/Split/net481/XElementPolyfill.cs | 6 +- src/Split/net481/ZipFilePolyfill.cs | 45 ++- .../ArgumentOutOfRangeExceptionPolyfill.cs | 12 +- src/Split/net5.0/ConvertPolyfill.cs | 28 +- src/Split/net5.0/DateTimeOffsetPolyfill.cs | 2 +- src/Split/net5.0/DateTimePolyfill.cs | 2 +- src/Split/net5.0/EqualityComparerPolyfill.cs | 20 +- src/Split/net5.0/FileUnixModePolyfill.cs | 8 +- src/Split/net5.0/Numbers/DoublePolyfill.cs | 8 +- src/Split/net5.0/Numbers/SinglePolyfill.cs | 8 +- src/Split/net5.0/Polyfill.cs | 2 +- src/Split/net5.0/Polyfill_List.cs | 8 + .../net5.0/Polyfill_MicroNanosecondAdd.cs | 4 +- src/Split/net5.0/Polyfill_String.cs | 43 ++- src/Split/net5.0/StringPolyfill.cs | 4 + src/Split/net5.0/TimeSpanPolyfill.cs | 63 ++-- src/Split/net5.0/ZipFilePolyfill.cs | 45 ++- .../ArgumentOutOfRangeExceptionPolyfill.cs | 12 +- src/Split/net6.0/ConvertPolyfill.cs | 28 +- src/Split/net6.0/DateTimeOffsetPolyfill.cs | 2 +- src/Split/net6.0/DateTimePolyfill.cs | 2 +- src/Split/net6.0/EqualityComparerPolyfill.cs | 20 +- src/Split/net6.0/FileUnixModePolyfill.cs | 8 +- src/Split/net6.0/Numbers/DoublePolyfill.cs | 8 +- src/Split/net6.0/Numbers/SinglePolyfill.cs | 8 +- src/Split/net6.0/Polyfill.cs | 2 +- src/Split/net6.0/Polyfill_List.cs | 8 + .../net6.0/Polyfill_MicroNanosecondAdd.cs | 4 +- src/Split/net6.0/Polyfill_String.cs | 2 - src/Split/net6.0/StringPolyfill.cs | 4 + src/Split/net6.0/TimeSpanPolyfill.cs | 63 ++-- src/Split/net6.0/ZipFilePolyfill.cs | 45 ++- .../ArgumentOutOfRangeExceptionPolyfill.cs | 10 +- src/Split/net7.0/ConvertPolyfill.cs | 28 +- src/Split/net7.0/EqualityComparerPolyfill.cs | 20 +- src/Split/net7.0/Numbers/DoublePolyfill.cs | 4 +- src/Split/net7.0/Numbers/SinglePolyfill.cs | 4 +- src/Split/net7.0/Polyfill.cs | 2 +- src/Split/net7.0/Polyfill_List.cs | 8 + src/Split/net7.0/Polyfill_String.cs | 2 - src/Split/net7.0/StringPolyfill.cs | 4 + src/Split/net7.0/TimeSpanPolyfill.cs | 61 ++-- src/Split/net7.0/ZipFilePolyfill.cs | 45 ++- src/Split/net8.0/ConvertPolyfill.cs | 28 +- src/Split/net8.0/EqualityComparerPolyfill.cs | 10 +- src/Split/net8.0/Polyfill.cs | 2 +- src/Split/net8.0/Polyfill_String.cs | 2 - src/Split/net8.0/StringPolyfill.cs | 4 + src/Split/net8.0/TimeSpanPolyfill.cs | 61 ++-- src/Split/net9.0/ConvertPolyfill.cs | 14 +- src/Split/net9.0/EqualityComparerPolyfill.cs | 10 +- src/Split/net9.0/Polyfill.cs | 2 +- src/Split/net9.0/Polyfill_String.cs | 2 - .../ArgumentOutOfRangeExceptionPolyfill.cs | 12 +- src/Split/netcoreapp2.0/ConvertPolyfill.cs | 28 +- .../netcoreapp2.0/DateTimeOffsetPolyfill.cs | 2 +- src/Split/netcoreapp2.0/DateTimePolyfill.cs | 2 +- .../netcoreapp2.0/EqualityComparerPolyfill.cs | 20 +- .../netcoreapp2.0/FileUnixModePolyfill.cs | 8 +- .../netcoreapp2.0/Numbers/DoublePolyfill.cs | 8 +- .../netcoreapp2.0/Numbers/SinglePolyfill.cs | 8 +- src/Split/netcoreapp2.0/Polyfill.cs | 2 +- .../Polyfill_Encoding_GetChars.cs | 2 +- src/Split/netcoreapp2.0/Polyfill_List.cs | 8 + .../netcoreapp2.0/Polyfill_Memory_SpanSort.cs | 8 +- .../Polyfill_MicroNanosecondAdd.cs | 4 +- src/Split/netcoreapp2.0/Polyfill_String.cs | 43 ++- .../Polyfill_StringBuilder_Append.cs | 8 + .../Polyfill_StringBuilder_CopyTo.cs | 31 +- src/Split/netcoreapp2.0/Polyfill_Type.cs | 31 +- .../RandomNumberGeneratorPolyfill.cs | 2 +- src/Split/netcoreapp2.0/StringPolyfill.cs | 4 + .../netcoreapp2.0/TaskCompletionSource.cs | 2 +- src/Split/netcoreapp2.0/TimeSpanPolyfill.cs | 63 ++-- src/Split/netcoreapp2.0/ZipFilePolyfill.cs | 45 ++- .../ArgumentOutOfRangeExceptionPolyfill.cs | 12 +- src/Split/netcoreapp2.1/ConvertPolyfill.cs | 28 +- .../netcoreapp2.1/DateTimeOffsetPolyfill.cs | 2 +- src/Split/netcoreapp2.1/DateTimePolyfill.cs | 2 +- .../netcoreapp2.1/EqualityComparerPolyfill.cs | 20 +- .../netcoreapp2.1/FileUnixModePolyfill.cs | 8 +- .../netcoreapp2.1/Numbers/DoublePolyfill.cs | 8 +- .../netcoreapp2.1/Numbers/SinglePolyfill.cs | 8 +- src/Split/netcoreapp2.1/Polyfill.cs | 2 +- src/Split/netcoreapp2.1/Polyfill_List.cs | 8 + .../netcoreapp2.1/Polyfill_Memory_SpanSort.cs | 8 +- .../Polyfill_MicroNanosecondAdd.cs | 4 +- src/Split/netcoreapp2.1/Polyfill_String.cs | 43 ++- .../RandomNumberGeneratorPolyfill.cs | 2 +- src/Split/netcoreapp2.1/StringPolyfill.cs | 4 + .../netcoreapp2.1/TaskCompletionSource.cs | 2 +- src/Split/netcoreapp2.1/TimeSpanPolyfill.cs | 63 ++-- src/Split/netcoreapp2.1/ZipFilePolyfill.cs | 45 ++- .../ArgumentOutOfRangeExceptionPolyfill.cs | 12 +- src/Split/netcoreapp2.2/ConvertPolyfill.cs | 28 +- .../netcoreapp2.2/DateTimeOffsetPolyfill.cs | 2 +- src/Split/netcoreapp2.2/DateTimePolyfill.cs | 2 +- .../netcoreapp2.2/EqualityComparerPolyfill.cs | 20 +- .../netcoreapp2.2/FileUnixModePolyfill.cs | 8 +- .../netcoreapp2.2/Numbers/DoublePolyfill.cs | 8 +- .../netcoreapp2.2/Numbers/SinglePolyfill.cs | 8 +- src/Split/netcoreapp2.2/Polyfill.cs | 2 +- src/Split/netcoreapp2.2/Polyfill_List.cs | 8 + .../netcoreapp2.2/Polyfill_Memory_SpanSort.cs | 8 +- .../Polyfill_MicroNanosecondAdd.cs | 4 +- src/Split/netcoreapp2.2/Polyfill_String.cs | 43 ++- .../RandomNumberGeneratorPolyfill.cs | 2 +- src/Split/netcoreapp2.2/StringPolyfill.cs | 4 + .../netcoreapp2.2/TaskCompletionSource.cs | 2 +- src/Split/netcoreapp2.2/TimeSpanPolyfill.cs | 63 ++-- src/Split/netcoreapp2.2/ZipFilePolyfill.cs | 45 ++- .../ArgumentOutOfRangeExceptionPolyfill.cs | 12 +- src/Split/netcoreapp3.0/ConvertPolyfill.cs | 28 +- .../netcoreapp3.0/DateTimeOffsetPolyfill.cs | 2 +- src/Split/netcoreapp3.0/DateTimePolyfill.cs | 2 +- .../netcoreapp3.0/EqualityComparerPolyfill.cs | 20 +- .../netcoreapp3.0/FileUnixModePolyfill.cs | 8 +- .../netcoreapp3.0/Numbers/DoublePolyfill.cs | 8 +- .../netcoreapp3.0/Numbers/SinglePolyfill.cs | 8 +- src/Split/netcoreapp3.0/Polyfill.cs | 2 +- src/Split/netcoreapp3.0/Polyfill_List.cs | 8 + .../netcoreapp3.0/Polyfill_Memory_SpanSort.cs | 8 +- .../Polyfill_MicroNanosecondAdd.cs | 4 +- src/Split/netcoreapp3.0/Polyfill_String.cs | 43 ++- src/Split/netcoreapp3.0/StringPolyfill.cs | 4 + .../netcoreapp3.0/TaskCompletionSource.cs | 2 +- src/Split/netcoreapp3.0/TimeSpanPolyfill.cs | 63 ++-- src/Split/netcoreapp3.0/ZipFilePolyfill.cs | 45 ++- .../ArgumentOutOfRangeExceptionPolyfill.cs | 12 +- src/Split/netcoreapp3.1/ConvertPolyfill.cs | 28 +- .../netcoreapp3.1/DateTimeOffsetPolyfill.cs | 2 +- src/Split/netcoreapp3.1/DateTimePolyfill.cs | 2 +- .../netcoreapp3.1/EqualityComparerPolyfill.cs | 20 +- .../netcoreapp3.1/FileUnixModePolyfill.cs | 8 +- .../netcoreapp3.1/Numbers/DoublePolyfill.cs | 8 +- .../netcoreapp3.1/Numbers/SinglePolyfill.cs | 8 +- src/Split/netcoreapp3.1/Polyfill.cs | 2 +- src/Split/netcoreapp3.1/Polyfill_List.cs | 8 + .../netcoreapp3.1/Polyfill_Memory_SpanSort.cs | 8 +- .../Polyfill_MicroNanosecondAdd.cs | 4 +- src/Split/netcoreapp3.1/Polyfill_String.cs | 43 ++- src/Split/netcoreapp3.1/StringPolyfill.cs | 4 + .../netcoreapp3.1/TaskCompletionSource.cs | 2 +- src/Split/netcoreapp3.1/TimeSpanPolyfill.cs | 63 ++-- src/Split/netcoreapp3.1/ZipFilePolyfill.cs | 45 ++- .../ArgumentOutOfRangeExceptionPolyfill.cs | 12 +- src/Split/netstandard2.0/ConvertPolyfill.cs | 28 +- .../netstandard2.0/DateTimeOffsetPolyfill.cs | 2 +- src/Split/netstandard2.0/DateTimePolyfill.cs | 2 +- .../EqualityComparerPolyfill.cs | 20 +- .../netstandard2.0/FileUnixModePolyfill.cs | 8 +- .../netstandard2.0/Numbers/DoublePolyfill.cs | 8 +- .../netstandard2.0/Numbers/SinglePolyfill.cs | 8 +- src/Split/netstandard2.0/Polyfill.cs | 2 +- .../netstandard2.0/Polyfill_ArraySegment.cs | 12 +- .../Polyfill_ConcurrentDictionary.cs | 4 + .../Polyfill_Encoding_GetChars.cs | 2 +- src/Split/netstandard2.0/Polyfill_List.cs | 8 + .../Polyfill_Memory_SpanSort.cs | 8 +- .../Polyfill_MicroNanosecondAdd.cs | 4 +- src/Split/netstandard2.0/Polyfill_String.cs | 43 ++- .../Polyfill_StringBuilder_Append.cs | 8 + .../Polyfill_StringBuilder_CopyTo.cs | 31 +- src/Split/netstandard2.0/Polyfill_Type.cs | 31 +- .../RandomNumberGeneratorPolyfill.cs | 2 +- src/Split/netstandard2.0/StringPolyfill.cs | 4 + .../netstandard2.0/TaskCompletionSource.cs | 2 +- src/Split/netstandard2.0/TimeSpanPolyfill.cs | 63 ++-- src/Split/netstandard2.0/XDocumentPolyfill.cs | 6 +- src/Split/netstandard2.0/XElementPolyfill.cs | 6 +- src/Split/netstandard2.0/ZipFilePolyfill.cs | 45 ++- .../ArgumentOutOfRangeExceptionPolyfill.cs | 12 +- src/Split/netstandard2.1/ConvertPolyfill.cs | 28 +- .../netstandard2.1/DateTimeOffsetPolyfill.cs | 2 +- src/Split/netstandard2.1/DateTimePolyfill.cs | 2 +- .../EqualityComparerPolyfill.cs | 20 +- .../netstandard2.1/FileUnixModePolyfill.cs | 8 +- .../netstandard2.1/Numbers/DoublePolyfill.cs | 8 +- .../netstandard2.1/Numbers/SinglePolyfill.cs | 8 +- src/Split/netstandard2.1/Polyfill.cs | 2 +- src/Split/netstandard2.1/Polyfill_List.cs | 8 + .../Polyfill_Memory_SpanSort.cs | 8 +- .../Polyfill_MicroNanosecondAdd.cs | 4 +- src/Split/netstandard2.1/Polyfill_String.cs | 43 ++- src/Split/netstandard2.1/StringPolyfill.cs | 4 + .../netstandard2.1/TaskCompletionSource.cs | 2 +- src/Split/netstandard2.1/TimeSpanPolyfill.cs | 63 ++-- src/Split/netstandard2.1/XDocumentPolyfill.cs | 6 +- src/Split/netstandard2.1/XElementPolyfill.cs | 6 +- src/Split/netstandard2.1/ZipFilePolyfill.cs | 45 ++- .../ArgumentOutOfRangeExceptionPolyfill.cs | 12 +- src/Split/uap10.0/ConvertPolyfill.cs | 28 +- src/Split/uap10.0/DateTimeOffsetPolyfill.cs | 2 +- src/Split/uap10.0/DateTimePolyfill.cs | 2 +- src/Split/uap10.0/EqualityComparerPolyfill.cs | 20 +- src/Split/uap10.0/FileUnixModePolyfill.cs | 8 +- src/Split/uap10.0/Numbers/DoublePolyfill.cs | 8 +- src/Split/uap10.0/Numbers/SinglePolyfill.cs | 8 +- src/Split/uap10.0/Polyfill.cs | 2 +- src/Split/uap10.0/Polyfill_ArraySegment.cs | 12 +- .../uap10.0/Polyfill_ConcurrentDictionary.cs | 4 + .../uap10.0/Polyfill_Encoding_GetChars.cs | 2 +- src/Split/uap10.0/Polyfill_List.cs | 8 + src/Split/uap10.0/Polyfill_Memory_SpanSort.cs | 8 +- .../uap10.0/Polyfill_MicroNanosecondAdd.cs | 4 +- src/Split/uap10.0/Polyfill_String.cs | 43 ++- .../uap10.0/Polyfill_StringBuilder_Append.cs | 8 + .../uap10.0/Polyfill_StringBuilder_CopyTo.cs | 31 +- src/Split/uap10.0/Polyfill_Type.cs | 31 +- .../uap10.0/RandomNumberGeneratorPolyfill.cs | 2 +- src/Split/uap10.0/StringPolyfill.cs | 4 + src/Split/uap10.0/TaskCompletionSource.cs | 2 +- src/Split/uap10.0/TimeSpanPolyfill.cs | 63 ++-- src/Split/uap10.0/XDocumentPolyfill.cs | 6 +- src/Split/uap10.0/XElementPolyfill.cs | 6 +- src/Split/uap10.0/ZipFilePolyfill.cs | 45 ++- src/Tests/PolyfillTests_Encoding.cs | 17 + src/Tests/PolyfillTests_Memory_Sort.cs | 16 + src/Tests/PolyfillTests_MicroNanosecond.cs | 35 +- src/Tests/PolyfillTests_Pass2.cs | 299 ++++++++++++++++++ src/Tests/PolyfillTests_String.cs | 17 + src/Tests/PolyfillTests_StringBuilder.cs | 27 ++ src/Tests/StringPolyfillTests.cs | 2 + 433 files changed, 4776 insertions(+), 2046 deletions(-) create mode 100644 src/Tests/PolyfillTests_Pass2.cs diff --git a/assemblySize.include.md b/assemblySize.include.md index a9b57af0b..726651d6c 100644 --- a/assemblySize.include.md +++ b/assemblySize.include.md @@ -2,26 +2,26 @@ | | Empty Assembly | With Polyfill | Diff | Ensure | ArgumentExceptions | StringInterpolation | Nullability | |----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------| -| netstandard2.0 | 8.0KB | 351.5KB | +343.5KB | +9.5KB | +6.5KB | +9.5KB | +14.0KB | -| netstandard2.1 | 8.5KB | 306.0KB | +297.5KB | +8.5KB | +6.0KB | +9.0KB | +13.5KB | -| net461 | 8.5KB | 350.5KB | +342.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net462 | 7.0KB | 354.0KB | +347.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net47 | 7.0KB | 353.5KB | +346.5KB | +9.0KB | +6.5KB | +9.5KB | +13.5KB | -| net471 | 8.5KB | 353.0KB | +344.5KB | +9.0KB | +6.0KB | +9.0KB | +13.5KB | -| net472 | 8.5KB | 351.5KB | +343.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net48 | 8.5KB | 351.5KB | +343.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net481 | 8.5KB | 351.5KB | +343.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| netcoreapp2.0 | 9.0KB | 327.5KB | +318.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp2.1 | 9.0KB | 308.0KB | +299.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| netcoreapp2.2 | 9.0KB | 308.0KB | +299.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| netcoreapp3.0 | 9.5KB | 301.0KB | +291.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp3.1 | 9.5KB | 299.0KB | +289.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net5.0 | 9.5KB | 263.0KB | +253.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net6.0 | 10.0KB | 205.0KB | +195.0KB | +9.5KB | +6.5KB | +512bytes | +3.0KB | -| net7.0 | 10.0KB | 167.0KB | +157.0KB | +9.5KB | +5.5KB | +1.0KB | +3.5KB | -| net8.0 | 9.5KB | 138.5KB | +129.0KB | +8.5KB | +512bytes | +512bytes | +3.5KB | +| netstandard2.0 | 8.0KB | 353.0KB | +345.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netstandard2.1 | 8.5KB | 306.5KB | +298.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net461 | 8.5KB | 351.5KB | +343.0KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB | +| net462 | 7.0KB | 355.0KB | +348.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net47 | 7.0KB | 355.0KB | +348.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net471 | 8.5KB | 354.0KB | +345.5KB | +8.0KB | +6.5KB | +9.0KB | +13.5KB | +| net472 | 8.5KB | 352.5KB | +344.0KB | +9.5KB | +6.5KB | +9.5KB | +14.0KB | +| net48 | 8.5KB | 352.5KB | +344.0KB | +9.5KB | +6.5KB | +9.5KB | +14.0KB | +| net481 | 8.5KB | 352.5KB | +344.0KB | +9.5KB | +6.5KB | +9.5KB | +14.0KB | +| netcoreapp2.0 | 9.0KB | 330.5KB | +321.5KB | +8.5KB | +6.0KB | +9.0KB | +13.5KB | +| netcoreapp2.1 | 9.0KB | 310.0KB | +301.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp2.2 | 9.0KB | 310.0KB | +301.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp3.0 | 9.5KB | 302.5KB | +293.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| netcoreapp3.1 | 9.5KB | 301.0KB | +291.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net5.0 | 9.5KB | 265.0KB | +255.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net6.0 | 10.0KB | 206.5KB | +196.5KB | +9.5KB | +7.0KB | +512bytes | +3.5KB | +| net7.0 | 10.0KB | 169.0KB | +159.0KB | +9.0KB | +5.5KB | +512bytes | +3.5KB | +| net8.0 | 9.5KB | 139.5KB | +130.0KB | +8.0KB | | +512bytes | +3.0KB | | net9.0 | 9.5KB | 92.5KB | +83.0KB | +8.5KB | | +512bytes | +3.5KB | -| net10.0 | 10.0KB | 70.0KB | +60.0KB | +9.0KB | | +512bytes | +3.5KB | +| net10.0 | 10.0KB | 70.0KB | +60.0KB | +9.0KB | | +1.0KB | +3.5KB | | net11.0 | 10.0KB | 31.5KB | +21.5KB | +9.0KB | | +512bytes | +3.5KB | @@ -29,24 +29,24 @@ | | Empty Assembly | With Polyfill | Diff | Ensure | ArgumentExceptions | StringInterpolation | Nullability | |----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------| -| netstandard2.0 | 8.0KB | 513.4KB | +505.4KB | +17.2KB | +8.2KB | +14.4KB | +19.4KB | -| netstandard2.1 | 8.5KB | 441.7KB | +433.2KB | +16.2KB | +7.7KB | +13.9KB | +18.9KB | -| net461 | 8.5KB | 513.5KB | +505.0KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net462 | 7.0KB | 517.0KB | +510.0KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net47 | 7.0KB | 516.2KB | +509.2KB | +16.7KB | +8.2KB | +14.4KB | +18.9KB | -| net471 | 8.5KB | 515.4KB | +506.9KB | +16.7KB | +7.7KB | +13.9KB | +18.9KB | -| net472 | 8.5KB | 512.8KB | +504.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net48 | 8.5KB | 512.8KB | +504.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net481 | 8.5KB | 512.8KB | +504.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| netcoreapp2.0 | 9.0KB | 478.9KB | +469.9KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp2.1 | 9.0KB | 447.4KB | +438.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| netcoreapp2.2 | 9.0KB | 447.4KB | +438.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| netcoreapp3.0 | 9.5KB | 431.6KB | +422.1KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp3.1 | 9.5KB | 429.6KB | +420.1KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net5.0 | 9.5KB | 375.4KB | +365.9KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net6.0 | 10.0KB | 297.4KB | +287.4KB | +17.2KB | +8.2KB | +1.1KB | +3.7KB | -| net7.0 | 10.0KB | 240.8KB | +230.8KB | +17.1KB | +6.9KB | +1.6KB | +4.2KB | -| net8.0 | 9.5KB | 197.3KB | +187.8KB | +16.0KB | +811bytes | +1.1KB | +4.2KB | -| net9.0 | 9.5KB | 130.3KB | +120.8KB | +16.0KB | | +1.1KB | +4.2KB | -| net10.0 | 10.0KB | 99.5KB | +89.5KB | +16.5KB | | +1.1KB | +4.2KB | +| netstandard2.0 | 8.0KB | 516.0KB | +508.0KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netstandard2.1 | 8.5KB | 442.9KB | +434.4KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net461 | 8.5KB | 515.6KB | +507.1KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | +| net462 | 7.0KB | 519.1KB | +512.1KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net47 | 7.0KB | 518.8KB | +511.8KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net471 | 8.5KB | 517.5KB | +509.0KB | +15.7KB | +8.2KB | +13.9KB | +18.9KB | +| net472 | 8.5KB | 514.9KB | +506.4KB | +17.2KB | +8.2KB | +14.4KB | +19.4KB | +| net48 | 8.5KB | 514.9KB | +506.4KB | +17.2KB | +8.2KB | +14.4KB | +19.4KB | +| net481 | 8.5KB | 514.9KB | +506.4KB | +17.2KB | +8.2KB | +14.4KB | +19.4KB | +| netcoreapp2.0 | 9.0KB | 482.9KB | +473.9KB | +16.2KB | +7.7KB | +13.9KB | +18.9KB | +| netcoreapp2.1 | 9.0KB | 450.2KB | +441.2KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp2.2 | 9.0KB | 450.2KB | +441.2KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp3.0 | 9.5KB | 433.9KB | +424.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| netcoreapp3.1 | 9.5KB | 432.4KB | +422.9KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net5.0 | 9.5KB | 378.2KB | +368.7KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net6.0 | 10.0KB | 299.6KB | +289.6KB | +17.2KB | +8.7KB | +1.1KB | +4.2KB | +| net7.0 | 10.0KB | 243.5KB | +233.5KB | +16.6KB | +6.9KB | +1.1KB | +4.2KB | +| net8.0 | 9.5KB | 198.5KB | +189.0KB | +15.5KB | +299bytes | +1.1KB | +3.7KB | +| net9.0 | 9.5KB | 130.4KB | +120.9KB | +16.0KB | | +1.1KB | +4.2KB | +| net10.0 | 10.0KB | 99.5KB | +89.5KB | +16.5KB | | +1.6KB | +4.2KB | | net11.0 | 10.0KB | 46.9KB | +36.9KB | +16.5KB | | +1.1KB | +4.2KB | diff --git a/readme.md b/readme.md index 50b1ea64c..199543e99 100644 --- a/readme.md +++ b/readme.md @@ -96,26 +96,26 @@ This project uses features from the newest stable SDK and C# language. As such c | | Empty Assembly | With Polyfill | Diff | Ensure | ArgumentExceptions | StringInterpolation | Nullability | |----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------| -| netstandard2.0 | 8.0KB | 351.5KB | +343.5KB | +9.5KB | +6.5KB | +9.5KB | +14.0KB | -| netstandard2.1 | 8.5KB | 306.0KB | +297.5KB | +8.5KB | +6.0KB | +9.0KB | +13.5KB | -| net461 | 8.5KB | 350.5KB | +342.0KB | +9.0KB | +6.0KB | +9.0KB | +13.5KB | -| net462 | 7.0KB | 354.0KB | +347.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net47 | 7.0KB | 353.5KB | +346.5KB | +9.0KB | +6.5KB | +9.5KB | +13.5KB | -| net471 | 8.5KB | 353.0KB | +344.5KB | +9.0KB | +6.0KB | +9.0KB | +13.5KB | -| net472 | 8.5KB | 351.5KB | +343.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net48 | 8.5KB | 351.5KB | +343.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| net481 | 8.5KB | 351.5KB | +343.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| netcoreapp2.0 | 9.0KB | 327.5KB | +318.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp2.1 | 9.0KB | 308.0KB | +299.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| netcoreapp2.2 | 9.0KB | 308.0KB | +299.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| netcoreapp3.0 | 9.5KB | 301.0KB | +291.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp3.1 | 9.5KB | 299.0KB | +289.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net5.0 | 9.5KB | 263.0KB | +253.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net6.0 | 10.0KB | 205.0KB | +195.0KB | +9.5KB | +6.5KB | +512bytes | +3.0KB | -| net7.0 | 10.0KB | 167.0KB | +157.0KB | +9.5KB | +5.5KB | +1.0KB | +3.5KB | -| net8.0 | 9.5KB | 138.5KB | +129.0KB | +8.5KB | +512bytes | +512bytes | +3.5KB | +| netstandard2.0 | 8.0KB | 353.0KB | +345.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netstandard2.1 | 8.5KB | 306.5KB | +298.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net461 | 8.5KB | 351.5KB | +343.0KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB | +| net462 | 7.0KB | 355.0KB | +348.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net47 | 7.0KB | 355.0KB | +348.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net471 | 8.5KB | 354.0KB | +345.5KB | +8.0KB | +6.5KB | +9.0KB | +13.5KB | +| net472 | 8.5KB | 352.5KB | +344.0KB | +9.5KB | +6.5KB | +9.5KB | +14.0KB | +| net48 | 8.5KB | 352.5KB | +344.0KB | +9.5KB | +6.5KB | +9.5KB | +14.0KB | +| net481 | 8.5KB | 352.5KB | +344.0KB | +9.5KB | +6.5KB | +9.5KB | +14.0KB | +| netcoreapp2.0 | 9.0KB | 330.5KB | +321.5KB | +8.5KB | +6.0KB | +9.0KB | +13.5KB | +| netcoreapp2.1 | 9.0KB | 310.0KB | +301.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp2.2 | 9.0KB | 310.0KB | +301.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp3.0 | 9.5KB | 302.5KB | +293.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| netcoreapp3.1 | 9.5KB | 301.0KB | +291.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net5.0 | 9.5KB | 265.0KB | +255.5KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| net6.0 | 10.0KB | 206.5KB | +196.5KB | +9.5KB | +7.0KB | +512bytes | +3.5KB | +| net7.0 | 10.0KB | 169.0KB | +159.0KB | +9.0KB | +5.5KB | +512bytes | +3.5KB | +| net8.0 | 9.5KB | 139.5KB | +130.0KB | +8.0KB | | +512bytes | +3.0KB | | net9.0 | 9.5KB | 92.5KB | +83.0KB | +8.5KB | | +512bytes | +3.5KB | -| net10.0 | 10.0KB | 70.0KB | +60.0KB | +9.0KB | | +512bytes | +3.5KB | +| net10.0 | 10.0KB | 70.0KB | +60.0KB | +9.0KB | | +1.0KB | +3.5KB | | net11.0 | 10.0KB | 31.5KB | +21.5KB | +9.0KB | | +512bytes | +3.5KB | @@ -123,26 +123,26 @@ This project uses features from the newest stable SDK and C# language. As such c | | Empty Assembly | With Polyfill | Diff | Ensure | ArgumentExceptions | StringInterpolation | Nullability | |----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------| -| netstandard2.0 | 8.0KB | 513.4KB | +505.4KB | +17.2KB | +8.2KB | +14.4KB | +19.4KB | -| netstandard2.1 | 8.5KB | 441.7KB | +433.2KB | +16.2KB | +7.7KB | +13.9KB | +18.9KB | -| net461 | 8.5KB | 513.5KB | +505.0KB | +16.7KB | +7.7KB | +13.9KB | +18.9KB | -| net462 | 7.0KB | 517.0KB | +510.0KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net47 | 7.0KB | 516.2KB | +509.2KB | +16.7KB | +8.2KB | +14.4KB | +18.9KB | -| net471 | 8.5KB | 515.4KB | +506.9KB | +16.7KB | +7.7KB | +13.9KB | +18.9KB | -| net472 | 8.5KB | 512.8KB | +504.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net48 | 8.5KB | 512.8KB | +504.3KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| net481 | 8.5KB | 512.8KB | +504.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| netcoreapp2.0 | 9.0KB | 478.9KB | +469.9KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp2.1 | 9.0KB | 447.4KB | +438.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| netcoreapp2.2 | 9.0KB | 447.4KB | +438.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| netcoreapp3.0 | 9.5KB | 431.6KB | +422.1KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp3.1 | 9.5KB | 429.6KB | +420.1KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net5.0 | 9.5KB | 375.4KB | +365.9KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net6.0 | 10.0KB | 297.4KB | +287.4KB | +17.2KB | +8.2KB | +1.1KB | +3.7KB | -| net7.0 | 10.0KB | 240.8KB | +230.8KB | +17.1KB | +6.9KB | +1.6KB | +4.2KB | -| net8.0 | 9.5KB | 197.3KB | +187.8KB | +16.0KB | +811bytes | +1.1KB | +4.2KB | -| net9.0 | 9.5KB | 130.3KB | +120.8KB | +16.0KB | | +1.1KB | +4.2KB | -| net10.0 | 10.0KB | 99.5KB | +89.5KB | +16.5KB | | +1.1KB | +4.2KB | +| netstandard2.0 | 8.0KB | 516.0KB | +508.0KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netstandard2.1 | 8.5KB | 442.9KB | +434.4KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net461 | 8.5KB | 515.6KB | +507.1KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | +| net462 | 7.0KB | 519.1KB | +512.1KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net47 | 7.0KB | 518.8KB | +511.8KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net471 | 8.5KB | 517.5KB | +509.0KB | +15.7KB | +8.2KB | +13.9KB | +18.9KB | +| net472 | 8.5KB | 514.9KB | +506.4KB | +17.2KB | +8.2KB | +14.4KB | +19.4KB | +| net48 | 8.5KB | 514.9KB | +506.4KB | +17.2KB | +8.2KB | +14.4KB | +19.4KB | +| net481 | 8.5KB | 514.9KB | +506.4KB | +17.2KB | +8.2KB | +14.4KB | +19.4KB | +| netcoreapp2.0 | 9.0KB | 482.9KB | +473.9KB | +16.2KB | +7.7KB | +13.9KB | +18.9KB | +| netcoreapp2.1 | 9.0KB | 450.2KB | +441.2KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp2.2 | 9.0KB | 450.2KB | +441.2KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp3.0 | 9.5KB | 433.9KB | +424.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| netcoreapp3.1 | 9.5KB | 432.4KB | +422.9KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net5.0 | 9.5KB | 378.2KB | +368.7KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net6.0 | 10.0KB | 299.6KB | +289.6KB | +17.2KB | +8.7KB | +1.1KB | +4.2KB | +| net7.0 | 10.0KB | 243.5KB | +233.5KB | +16.6KB | +6.9KB | +1.1KB | +4.2KB | +| net8.0 | 9.5KB | 198.5KB | +189.0KB | +15.5KB | +299bytes | +1.1KB | +3.7KB | +| net9.0 | 9.5KB | 130.4KB | +120.9KB | +16.0KB | | +1.1KB | +4.2KB | +| net10.0 | 10.0KB | 99.5KB | +89.5KB | +16.5KB | | +1.6KB | +4.2KB | | net11.0 | 10.0KB | 46.9KB | +36.9KB | +16.5KB | | +1.1KB | +4.2KB | diff --git a/src/Polyfill/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs b/src/Polyfill/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs index 832bcad0c..3a4418931 100644 --- a/src/Polyfill/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs +++ b/src/Polyfill/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs @@ -19,7 +19,7 @@ public static void ThrowIfZero(T value, [CallerArgumentExpression(nameof(valu { if (T.IsZero(value)) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } #else @@ -27,7 +27,7 @@ public static void ThrowIfZero(T value, [CallerArgumentExpression(nameof(valu { if (value.Equals(default)) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } #endif @@ -38,14 +38,14 @@ public static void ThrowIfZero(nint value, [CallerArgumentExpression(nameof(valu { if (value == 0) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } #endif [DoesNotReturn] - static void ThrowZero(string? paramName) => - throw new ArgumentOutOfRangeException(paramName, "Value must not be zero."); + static void ThrowZero(T value, string? paramName) => + throw new ArgumentOutOfRangeException(paramName, value, $"{paramName} ('{value}') must be a non-zero value."); #endif #if !NET8_0_OR_GREATER @@ -191,7 +191,7 @@ public static void ThrowIfGreaterThanOrEqual(nint value, nint other, [CallerArgu { if (value >= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than {other}."); } } #endif @@ -237,7 +237,7 @@ public static void ThrowIfLessThanOrEqual(nint value, nint other, [CallerArgumen { if (value <= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than {other}."); } } #endif diff --git a/src/Polyfill/ConvertPolyfill.cs b/src/Polyfill/ConvertPolyfill.cs index da463d6ef..c2e616328 100644 --- a/src/Polyfill/ConvertPolyfill.cs +++ b/src/Polyfill/ConvertPolyfill.cs @@ -176,20 +176,23 @@ public static OperationStatus FromHexString(ReadOnlySpan source, Span= destination.Length) { charsConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal(source[i]); + var lo = GetHexVal(source[i + 1]); + if (hi < 0 || lo < 0) { - charsConsumed = i; + // The BCL counts a valid leading nibble as consumed: it stops at i+1 unless the + // low nibble is the valid one (i.e. the high nibble is the invalid character). + charsConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); @@ -248,20 +251,23 @@ public static OperationStatus FromHexString(ReadOnlySpan utf8Source, Span< var j = 0; while (i + 1 < utf8Source.Length) { - var hi = GetHexVal((char)utf8Source[i]); - var lo = GetHexVal((char)utf8Source[i + 1]); - if (hi < 0 || lo < 0) + // The BCL gives DestinationTooSmall priority over InvalidData when the buffer is full. + if (j >= destination.Length) { bytesConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal((char)utf8Source[i]); + var lo = GetHexVal((char)utf8Source[i + 1]); + if (hi < 0 || lo < 0) { - bytesConsumed = i; + // The BCL counts a valid leading nibble as consumed: it stops at i+1 unless the + // low nibble is the valid one (i.e. the high nibble is the invalid character). + bytesConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); diff --git a/src/Polyfill/DateTimeOffsetPolyfill.cs b/src/Polyfill/DateTimeOffsetPolyfill.cs index f159c3291..de9d74a2a 100644 --- a/src/Polyfill/DateTimeOffsetPolyfill.cs +++ b/src/Polyfill/DateTimeOffsetPolyfill.cs @@ -26,7 +26,7 @@ static partial class Polyfill /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset.microsecond?view=net-11.0 public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { diff --git a/src/Polyfill/DateTimePolyfill.cs b/src/Polyfill/DateTimePolyfill.cs index a413bccf6..5b3c4361d 100644 --- a/src/Polyfill/DateTimePolyfill.cs +++ b/src/Polyfill/DateTimePolyfill.cs @@ -22,7 +22,7 @@ static partial class Polyfill /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.datetime.microsecond?view=net-11.0 public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { diff --git a/src/Polyfill/EqualityComparerPolyfill.cs b/src/Polyfill/EqualityComparerPolyfill.cs index 046f71151..c2706e950 100644 --- a/src/Polyfill/EqualityComparerPolyfill.cs +++ b/src/Polyfill/EqualityComparerPolyfill.cs @@ -23,8 +23,15 @@ public override int GetHashCode(T obj) => //Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.equalitycomparer-1.create?view=net-11.0 public static EqualityComparer Create( Func equals, - Func? getHashCode = null) => - new DelegateEqualityComparer(equals, getHashCode); + Func? getHashCode = null) + { + if (equals is null) + { + throw new ArgumentNullException(nameof(equals)); + } + + return new DelegateEqualityComparer(equals, getHashCode); + } } #endif @@ -52,8 +59,15 @@ public override int GetHashCode(T obj) //Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.equalitycomparer-1.create?view=net-11.0#system-collections-generic-equalitycomparer-1-create-1(system-func((-0-0))-system-collections-generic-iequalitycomparer((-0))) public static EqualityComparer Create( Func keySelector, - IEqualityComparer? keyComparer = null) => - new KeySelectorEqualityComparer(keySelector, keyComparer); + IEqualityComparer? keyComparer = null) + { + if (keySelector is null) + { + throw new ArgumentNullException(nameof(keySelector)); + } + + return new KeySelectorEqualityComparer(keySelector, keyComparer); + } } #endif } diff --git a/src/Polyfill/FileUnixModePolyfill.cs b/src/Polyfill/FileUnixModePolyfill.cs index 4ed9ae73c..bc5e644cc 100644 --- a/src/Polyfill/FileUnixModePolyfill.cs +++ b/src/Polyfill/FileUnixModePolyfill.cs @@ -57,8 +57,8 @@ public static UnixFileMode GetUnixFileMode(string path) var mode = output[0] switch { '1' => UnixFileMode.StickyBit, - '2' => UnixFileMode.SetUser, - '4' => UnixFileMode.SetGroup, + '2' => UnixFileMode.SetGroup, + '4' => UnixFileMode.SetUser, _ => UnixFileMode.None }; @@ -181,13 +181,13 @@ public static void SetUnixFileMode(string path, UnixFileMode mode) octal[3] += 1; break; case "setuser": - octal[0] += 2; + octal[0] += 4; break; case "stickybit": octal[0] += 1; break; case "setgroup": - octal[0] += 4; + octal[0] += 2; break; default: throw new("Invalid notation detected"); diff --git a/src/Polyfill/Numbers/DoublePolyfill.cs b/src/Polyfill/Numbers/DoublePolyfill.cs index 592d359c4..923480ec4 100644 --- a/src/Polyfill/Numbers/DoublePolyfill.cs +++ b/src/Polyfill/Numbers/DoublePolyfill.cs @@ -18,7 +18,7 @@ static partial class Polyfill /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.double.tryparse?view=net-11.0#system-double-tryparse(system-string-system-iformatprovider-system-double@) public static bool TryParse(string? s, IFormatProvider? provider, out double result) => - double.TryParse(s, NumberStyles.Float, provider, out result); + double.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif @@ -30,7 +30,7 @@ public static bool TryParse(string? s, IFormatProvider? provider, out double res /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.double.tryparse?view=net-11.0#system-double-tryparse(system-readonlyspan((system-byte))-system-iformatprovider-system-double@) public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. @@ -44,7 +44,7 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.double.tryparse?view=net-11.0#system-double-tryparse(system-readonlyspan((system-byte))-system-double@) public static bool TryParse(ReadOnlySpan utf8Text, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); #endif @@ -73,7 +73,7 @@ public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatPro /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.double.tryparse?view=net-11.0#system-double-tryparse(system-readonlyspan((system-char))-system-iformatprovider-system-double@) public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out double result) => - double.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + double.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif diff --git a/src/Polyfill/Numbers/SinglePolyfill.cs b/src/Polyfill/Numbers/SinglePolyfill.cs index 39a621a2d..140868d76 100644 --- a/src/Polyfill/Numbers/SinglePolyfill.cs +++ b/src/Polyfill/Numbers/SinglePolyfill.cs @@ -18,7 +18,7 @@ static partial class Polyfill /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.single.tryparse?view=net-11.0#system-single-tryparse(system-string-system-iformatprovider-system-single@) public static bool TryParse(string? s, IFormatProvider? provider, out float result) => - float.TryParse(s, NumberStyles.Float, provider, out result); + float.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif @@ -30,7 +30,7 @@ public static bool TryParse(string? s, IFormatProvider? provider, out float resu /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.single.tryparse?view=net-11.0#system-single-tryparse(system-readonlyspan((system-byte))-system-iformatprovider-system-single@) public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. @@ -44,7 +44,7 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.single.tryparse?view=net-11.0#system-single-tryparse(system-readonlyspan((system-byte))-system-single@) public static bool TryParse(ReadOnlySpan utf8Text, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); #endif @@ -73,7 +73,7 @@ public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatPro /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.single.tryparse?view=net-11.0#system-single-tryparse(system-readonlyspan((system-char))-system-iformatprovider-system-single@) public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out float result) => - float.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + float.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif diff --git a/src/Polyfill/Polyfill.cs b/src/Polyfill/Polyfill.cs index 2f564060e..0b9b2bdfb 100644 --- a/src/Polyfill/Polyfill.cs +++ b/src/Polyfill/Polyfill.cs @@ -18,5 +18,5 @@ namespace Polyfills; #endif static partial class Polyfill { - const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond * 1000; + const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; } \ No newline at end of file diff --git a/src/Polyfill/Polyfill_ArraySegment.cs b/src/Polyfill/Polyfill_ArraySegment.cs index e426b6d29..408b02a7f 100644 --- a/src/Polyfill/Polyfill_ArraySegment.cs +++ b/src/Polyfill/Polyfill_ArraySegment.cs @@ -16,17 +16,22 @@ static partial class Polyfill //Link: https://learn.microsoft.com/en-us/dotnet/api/system.arraysegment-1.copyto?view=net-11.0#system-arraysegment-1-copyto(-0()) public static void CopyTo(this ArraySegment target, ArraySegment destination) { - if (target.Count > destination.Count) + if (target.Array == null) { - throw new ArgumentException("DestinationTooShort", nameof(destination)); + throw new InvalidOperationException("InvalidOperation_NullArray"); } - if (target.Array == null) + if (destination.Array == null) { throw new InvalidOperationException("InvalidOperation_NullArray"); } - Array.Copy(target.Array!, target.Offset, destination.Array, destination.Offset, target.Count); + if (target.Count > destination.Count) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } + + Array.Copy(target.Array, target.Offset, destination.Array, destination.Offset, target.Count); } /// diff --git a/src/Polyfill/Polyfill_ConcurrentDictionary.cs b/src/Polyfill/Polyfill_ConcurrentDictionary.cs index ceadbbf08..f6c74355d 100644 --- a/src/Polyfill/Polyfill_ConcurrentDictionary.cs +++ b/src/Polyfill/Polyfill_ConcurrentDictionary.cs @@ -15,6 +15,11 @@ static partial class Polyfill public static TValue GetOrAdd(this ConcurrentDictionary target, TKey key, Func valueFactory, TArg factoryArgument) where TKey : notnull { + if (valueFactory == null) + { + throw new ArgumentNullException(nameof(valueFactory)); + } + while (true) { TValue value; diff --git a/src/Polyfill/Polyfill_Encoding_GetChars.cs b/src/Polyfill/Polyfill_Encoding_GetChars.cs index e93293b62..574165e14 100644 --- a/src/Polyfill/Polyfill_Encoding_GetChars.cs +++ b/src/Polyfill/Polyfill_Encoding_GetChars.cs @@ -28,7 +28,7 @@ public static int GetChars(this Encoding target, ReadOnlySpan bytes, Span< var charArray = new char[bytes.Length]; var array = bytes.ToArray(); var count = target.GetChars(array, 0, bytes.Length, charArray, 0); - new ReadOnlySpan(charArray).CopyTo(chars); + new ReadOnlySpan(charArray, 0, count).CopyTo(chars); return count; } #endif diff --git a/src/Polyfill/Polyfill_List.cs b/src/Polyfill/Polyfill_List.cs index 32cbd23b0..78bf2b0f2 100644 --- a/src/Polyfill/Polyfill_List.cs +++ b/src/Polyfill/Polyfill_List.cs @@ -30,6 +30,11 @@ public static void AddRange(this List target, ReadOnlySpan source) //Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.collectionextensions.insertrange?view=net-11.0 public static void InsertRange(this List target, int index, ReadOnlySpan source) { + if ((uint)index > (uint)target.Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + for (var i = 0; i < source.Length; i++) { var item = source[i]; @@ -41,6 +46,11 @@ public static void InsertRange(this List target, int index, ReadOnlySpan(this List target, Span destination) { + if (target.Count > destination.Length) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } + for (var index = 0; index < target.Count; index++) { destination[index] = target[index]; diff --git a/src/Polyfill/Polyfill_Memory_SpanSort.cs b/src/Polyfill/Polyfill_Memory_SpanSort.cs index 40640f221..6b60458ba 100644 --- a/src/Polyfill/Polyfill_Memory_SpanSort.cs +++ b/src/Polyfill/Polyfill_Memory_SpanSort.cs @@ -99,13 +99,11 @@ internal ComparerWrapper(Comparison comparison) this.comparison = comparison; } - public int Compare(T? x, T? y) - { - if (x is null || y is null) - return 0; - - return comparison((T)x, (T)y); - } + // Delegate straight to the user comparison, including null keys, to match the BCL + // (which lets the comparison decide how nulls order). Short-circuiting nulls to 0 + // would treat every null-involving pair as equal and leave nulls unsorted. + public int Compare(T? x, T? y) => + comparison(x!, y!); } } diff --git a/src/Polyfill/Polyfill_MicroNanosecondAdd.cs b/src/Polyfill/Polyfill_MicroNanosecondAdd.cs index 8e32aed68..29ad4aec0 100644 --- a/src/Polyfill/Polyfill_MicroNanosecondAdd.cs +++ b/src/Polyfill/Polyfill_MicroNanosecondAdd.cs @@ -6,18 +6,22 @@ namespace Polyfills; static partial class Polyfill { + // 1 microsecond == 10 ticks (1 tick == 100ns). Routing through AddTicks preserves + // sub-millisecond precision; AddMilliseconds rounds its argument to whole milliseconds + // on .NET Framework / netcoreapp2.0, which would discard it. + /// /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.datetime.addmicroseconds?view=net-11.0 public static DateTime AddMicroseconds(this DateTime target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); /// /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset.addmicroseconds?view=net-11.0 public static DateTimeOffset AddMicroseconds(this DateTimeOffset target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); } #endif diff --git a/src/Polyfill/Polyfill_String.cs b/src/Polyfill/Polyfill_String.cs index 95efc5c91..0e16714a4 100644 --- a/src/Polyfill/Polyfill_String.cs +++ b/src/Polyfill/Polyfill_String.cs @@ -2,8 +2,6 @@ namespace Polyfills; using System; using System.Text; -using System.IO; -using System.Runtime.CompilerServices; static partial class Polyfill { @@ -202,24 +200,56 @@ public static int LastIndexOf(this string target, char value, int startIndex, in /// Replaces all newline sequences in the current string with . /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.string.replacelineendings?view=net-11.0#system-string-replacelineendings(system-string) - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ReplaceLineEndings(this string target, string replacementText) { + var index = IndexOfNewlineChar(target, 0, out var stride); + if (index < 0) + { + // No line endings to replace; matches the BCL fast path. + return target; + } + var builder = new StringBuilder(target.Length); - using var reader = new StringReader(target); + var position = 0; while (true) { - var line = reader.ReadLine(); - if (line == null) + builder.Append(target, position, index - position); + builder.Append(replacementText); + position = index + stride; + index = IndexOfNewlineChar(target, position, out stride); + if (index < 0) { break; } + } - builder.Append(line); - builder.Append(replacementText); + builder.Append(target, position, target.Length - position); + return builder.ToString(); + } + + // Mirrors the line-ending set recognised by string.ReplaceLineEndings: + // CR, LF, CRLF, FF (U+000C), NEL (U+0085), LS (U+2028) and PS (U+2029). + static int IndexOfNewlineChar(string text, int startIndex, out int stride) + { + stride = 0; + for (var index = startIndex; index < text.Length; index++) + { + switch (text[index]) + { + case '\r': + stride = index + 1 < text.Length && text[index + 1] == '\n' ? 2 : 1; + return index; + case '\n': + case '\f': + case '\u0085': + case '\u2028': + case '\u2029': + stride = 1; + return index; + } } - return builder.ToString(0, builder.Length - replacementText.Length); + return -1; } /// diff --git a/src/Polyfill/Polyfill_StringBuilder_Append.cs b/src/Polyfill/Polyfill_StringBuilder_Append.cs index 259886770..854bec4bc 100644 --- a/src/Polyfill/Polyfill_StringBuilder_Append.cs +++ b/src/Polyfill/Polyfill_StringBuilder_Append.cs @@ -13,6 +13,16 @@ static partial class Polyfill //Link: https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.append?view=net-11.0#system-text-stringbuilder-append(system-text-stringbuilder-system-int32-system-int32) public static StringBuilder Append(this StringBuilder target, StringBuilder? value, int startIndex, int count) { + if (startIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(startIndex)); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (value == null) { if (startIndex == 0 && count == 0) diff --git a/src/Polyfill/Polyfill_StringBuilder_CopyTo.cs b/src/Polyfill/Polyfill_StringBuilder_CopyTo.cs index b0206d22b..8733a07dc 100644 --- a/src/Polyfill/Polyfill_StringBuilder_CopyTo.cs +++ b/src/Polyfill/Polyfill_StringBuilder_CopyTo.cs @@ -17,22 +17,29 @@ public static void CopyTo( Span destination, int count) { - var destinationIndex = 0; - while (true) + if (count < 0) { - if (sourceIndex == target.Length) - { - break; - } - - if (destinationIndex == count) - { - break; - } - - destination[destinationIndex] = target[sourceIndex]; - destinationIndex++; - sourceIndex++; + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if ((uint)sourceIndex > (uint)target.Length) + { + throw new ArgumentOutOfRangeException(nameof(sourceIndex)); + } + + if (sourceIndex > target.Length - count) + { + throw new ArgumentException("The source StringBuilder does not contain enough characters from sourceIndex onward to satisfy count."); + } + + if (count > destination.Length) + { + throw new ArgumentException("The destination span is too short to hold the requested characters.", nameof(destination)); + } + + for (var index = 0; index < count; index++) + { + destination[index] = target[sourceIndex + index]; } } } diff --git a/src/Polyfill/Polyfill_Type.cs b/src/Polyfill/Polyfill_Type.cs index 584e5a02d..bbf27f5d3 100644 --- a/src/Polyfill/Polyfill_Type.cs +++ b/src/Polyfill/Polyfill_Type.cs @@ -9,9 +9,16 @@ static partial class Polyfill { #if !NETCOREAPP2_1_OR_GREATER && !NETSTANDARD2_1_OR_GREATER //Link: https://learn.microsoft.com/en-us/dotnet/api/system.reflection.memberinfo.hassamemetadatadefinitionas?view=net-11.0 - public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInfo other) => - target.MetadataToken == other.MetadataToken && - target.Module.Equals(other.Module); + public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInfo other) + { + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } + + return target.MetadataToken == other.MetadataToken && + target.Module.Equals(other.Module); + } #endif #if NETFRAMEWORK || NETSTANDARD2_0 || NETCOREAPP2_0 || WINDOWS_UWP @@ -28,6 +35,29 @@ public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInf Type[] types, ParameterModifier[]? modifiers) { + if (name is null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (genericParameterCount < 0) + { + throw new ArgumentOutOfRangeException(nameof(genericParameterCount)); + } + + if (types is null) + { + throw new ArgumentNullException(nameof(types)); + } + + foreach (var type in types) + { + if (type is null) + { + throw new ArgumentNullException(nameof(types)); + } + } + var methods = target.GetMethods(bindingAttr); if (genericParameterCount == 0) { diff --git a/src/Polyfill/RandomNumberGeneratorPolyfill.cs b/src/Polyfill/RandomNumberGeneratorPolyfill.cs index 6ca116f58..1cbae36b9 100644 --- a/src/Polyfill/RandomNumberGeneratorPolyfill.cs +++ b/src/Polyfill/RandomNumberGeneratorPolyfill.cs @@ -50,7 +50,7 @@ public static int GetInt32(int fromInclusive, int toExclusive) { generator.GetBytes(bytes); value = BitConverter.ToUInt32(bytes, 0) & mask; - } while (value >= range); + } while (value > range); return (int) (fromInclusive + value); } diff --git a/src/Polyfill/StringPolyfill.cs b/src/Polyfill/StringPolyfill.cs index cb8865694..eb3751d9f 100644 --- a/src/Polyfill/StringPolyfill.cs +++ b/src/Polyfill/StringPolyfill.cs @@ -69,6 +69,11 @@ public static string Join(char separator, scoped ReadOnlySpan values) var value = values[index]; + if (value is null) + { + continue; + } + value.CopyTo(span); span = span.Slice(value.Length); diff --git a/src/Polyfill/TaskCompletionSource.cs b/src/Polyfill/TaskCompletionSource.cs index a4d8dc5f1..52d0de09a 100644 --- a/src/Polyfill/TaskCompletionSource.cs +++ b/src/Polyfill/TaskCompletionSource.cs @@ -98,6 +98,6 @@ public TaskCompletionSource(object? state, TaskCreationOptions creationOptions) /// /// Attempts to transition the underlying into the state. /// - public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(default); + public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(cancellationToken); } #endif diff --git a/src/Polyfill/TimeSpanPolyfill.cs b/src/Polyfill/TimeSpanPolyfill.cs index be522c126..28c1b58f4 100644 --- a/src/Polyfill/TimeSpanPolyfill.cs +++ b/src/Polyfill/TimeSpanPolyfill.cs @@ -19,7 +19,7 @@ static partial class Polyfill /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.timespan.microseconds?view=net-11.0 public int Microseconds => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { @@ -32,6 +32,27 @@ long TicksComponent() #if !NET9_0_OR_GREATER const long MicrosecondsToTicks = TimeSpan.TicksPerMillisecond / 1000; + const long MicrosecondsPerMillisecond = 1_000; + const long MicrosecondsPerSecond = 1_000_000; + const long MicrosecondsPerMinute = 60_000_000; + const long MicrosecondsPerHour = 3_600_000_000; + const long MicrosecondsPerDay = 86_400_000_000; + + // The BCL From* factories accumulate every component in microseconds in a 128-bit-wide space + // and range-check only the FINAL tick count, throwing ArgumentOutOfRangeException on overflow. + // Int64 microsecond accumulation overflows too early for the long overloads when components + // cancel into a valid range, so accumulate in decimal (exact for these magnitudes) to mirror + // the BCL. The message matches the BCL's Overflow_TimeSpanTooLong. + static TimeSpan TicksFromMicroseconds(decimal totalMicroseconds) + { + var ticks = totalMicroseconds * MicrosecondsToTicks; + if (ticks < long.MinValue || ticks > long.MaxValue) + { + throw new ArgumentOutOfRangeException(null, "TimeSpan overflowed because the duration is too long."); + } + + return new((long)ticks); + } extension(TimeSpan) { @@ -40,57 +61,62 @@ long TicksComponent() /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.timespan.fromdays?view=net-11.0#system-timespan-fromdays(system-int32-system-int32-system-int32-system-int32-system-int32-system-int32) public static TimeSpan FromDays(int days, int hours = 0, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)days * TimeSpan.TicksPerDay + - (long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)days * MicrosecondsPerDay + + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of hours, minutes, seconds, milliseconds, and microseconds. /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.timespan.fromhours?view=net-11.0#system-timespan-fromhours(system-int32-system-int32-system-int32-system-int32-system-int32) public static TimeSpan FromHours(int hours, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of minutes, seconds, milliseconds, and microseconds. /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.timespan.fromminutes?view=net-11.0#system-timespan-fromminutes(system-int32-system-int32-system-int32-system-int32) public static TimeSpan FromMinutes(int minutes, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of seconds, milliseconds, and microseconds. /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.timespan.fromseconds?view=net-11.0#system-timespan-fromseconds(system-int64-system-int64-system-int64) public static TimeSpan FromSeconds(long seconds, long milliseconds = 0, long microseconds = 0) => - new(seconds * TimeSpan.TicksPerSecond + - milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of milliseconds and microseconds. /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.timespan.frommilliseconds?view=net-11.0#system-timespan-frommilliseconds(system-int64-system-int64) public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0) => - new(milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of microseconds. /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.timespan.frommicroseconds?view=net-11.0#system-timespan-frommicroseconds(system-int64) public static TimeSpan FromMicroseconds(long microseconds) => - new(microseconds * MicrosecondsToTicks); + TicksFromMicroseconds(microseconds); } #endif } diff --git a/src/Polyfill/XDocumentPolyfill.cs b/src/Polyfill/XDocumentPolyfill.cs index 923805f76..ec535a278 100644 --- a/src/Polyfill/XDocumentPolyfill.cs +++ b/src/Polyfill/XDocumentPolyfill.cs @@ -29,12 +29,12 @@ static partial class XDocumentPolyfill /// Asynchronously creates a new XDocument and initializes its underlying XML tree using the specified stream, optionally preserving white space. /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.xml.linq.xdocument.loadasync?view=net-11.0#system-xml-linq-xdocument-loadasync(system-io-stream-system-xml-linq-loadoptions-system-threading-cancellationtoken) - public static async Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) + public static Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - using var reader = new StreamReader(stream); - var content = await reader.ReadToEndAsync(); - return XDocument.Parse(content, options); + // Load from the stream directly so the XML-declared encoding / BOM is honored, + // rather than forcing UTF-8 decoding via StreamReader. + return Task.FromResult(XDocument.Load(stream, options)); } /// diff --git a/src/Polyfill/XElementPolyfill.cs b/src/Polyfill/XElementPolyfill.cs index ad3204fca..8d3ad5fa6 100644 --- a/src/Polyfill/XElementPolyfill.cs +++ b/src/Polyfill/XElementPolyfill.cs @@ -29,12 +29,12 @@ static partial class XElementPolyfill /// Asynchronously creates a new XElement and initializes its underlying XML tree using the specified stream, optionally preserving white space. /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.xml.linq.xelement.loadasync?view=net-11.0#system-xml-linq-xelement-loadasync(system-io-stream-system-xml-linq-loadoptions-system-threading-cancellationtoken) - public static async Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) + public static Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - using var reader = new StreamReader(stream); - var content = await reader.ReadToEndAsync(); - return XElement.Parse(content, options); + // Load from the stream directly so the XML-declared encoding / BOM is honored, + // rather than forcing UTF-8 decoding via StreamReader. + return Task.FromResult(XElement.Load(stream, options)); } /// diff --git a/src/Polyfill/ZLibStream.cs b/src/Polyfill/ZLibStream.cs index 35acef938..a7ef4f327 100644 --- a/src/Polyfill/ZLibStream.cs +++ b/src/Polyfill/ZLibStream.cs @@ -127,6 +127,10 @@ public override int Read(byte[] buffer, int offset, int count) ReadHeader(); + // Note: unlike the BCL ZLibStream, the trailing Adler-32 checksum is not validated on + // decompression. The payload is still inflated correctly, but a corrupted checksum is not + // detected (the BCL throws InvalidDataException). Reliable validation is not achievable on + // top of DeflateStream, which may buffer past the end of the deflate payload. return deflateStream!.Read(buffer, offset, count); } diff --git a/src/Polyfill/ZipFilePolyfill.cs b/src/Polyfill/ZipFilePolyfill.cs index 1bdc79f5e..339cc0fed 100644 --- a/src/Polyfill/ZipFilePolyfill.cs +++ b/src/Polyfill/ZipFilePolyfill.cs @@ -66,11 +66,13 @@ static void ExtractToDirectoryPolyfill( #if NET8_0_OR_GREATER ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, overwrite); #else - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + return; } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, null); #endif } @@ -83,14 +85,60 @@ static void ExtractToDirectoryPolyfill( #if NET8_0_OR_GREATER ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding, overwrite); #else - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); + return; } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); + + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, encoding); #endif } +#if !NET8_0_OR_GREATER + // Mirrors the BCL overwriteFiles:true behavior: overwrite conflicting files in place, + // leaving any other pre-existing files in the destination untouched, rather than + // deleting the entire destination directory first. + static void ExtractToDirectoryWithOverwrite( + string sourceArchiveFile, + string destinationDirectory, + Encoding? encoding) + { + var destinationRoot = Path.GetFullPath(destinationDirectory); + Directory.CreateDirectory(destinationRoot); + + var normalizedRoot = destinationRoot; + if (normalizedRoot[normalizedRoot.Length - 1] != Path.DirectorySeparatorChar) + { + normalizedRoot += Path.DirectorySeparatorChar; + } + + using var archive = encoding == null + ? ZipFile.OpenRead(sourceArchiveFile) + : ZipFile.Open(sourceArchiveFile, ZipArchiveMode.Read, encoding); + foreach (var entry in archive.Entries) + { + var destinationPath = Path.GetFullPath(Path.Combine(destinationRoot, entry.FullName)); + + // Guard against entries that would escape the destination directory (zip slip). + if (!destinationPath.StartsWith(normalizedRoot, StringComparison.Ordinal)) + { + throw new IOException("Extracting Zip entry would have resulted in a file outside the specified destination directory."); + } + + if (entry.Name.Length == 0) + { + // The entry is a directory. + Directory.CreateDirectory(destinationPath); + continue; + } + + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + entry.ExtractToFile(destinationPath, overwrite: true); + } + } +#endif + /// /// Asynchronously opens a ZipArchive on the specified archiveFileName in the specified ZipArchiveMode mode. /// diff --git a/src/Split/net10.0/EqualityComparerPolyfill.cs b/src/Split/net10.0/EqualityComparerPolyfill.cs index 41245747e..035fc238b 100644 --- a/src/Split/net10.0/EqualityComparerPolyfill.cs +++ b/src/Split/net10.0/EqualityComparerPolyfill.cs @@ -24,7 +24,13 @@ public override int GetHashCode(T obj) /// public static EqualityComparer Create( Func keySelector, - IEqualityComparer? keyComparer = null) => - new KeySelectorEqualityComparer(keySelector, keyComparer); + IEqualityComparer? keyComparer = null) + { + if (keySelector is null) + { + throw new ArgumentNullException(nameof(keySelector)); + } + return new KeySelectorEqualityComparer(keySelector, keyComparer); + } } } diff --git a/src/Split/net10.0/Polyfill.cs b/src/Split/net10.0/Polyfill.cs index 16e916262..b7b21af17 100644 --- a/src/Split/net10.0/Polyfill.cs +++ b/src/Split/net10.0/Polyfill.cs @@ -17,5 +17,5 @@ namespace Polyfills; #endif static partial class Polyfill { - const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond * 1000; + const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; } diff --git a/src/Split/net10.0/Polyfill_String.cs b/src/Split/net10.0/Polyfill_String.cs index dc72e38f7..99ec08a0c 100644 --- a/src/Split/net10.0/Polyfill_String.cs +++ b/src/Split/net10.0/Polyfill_String.cs @@ -3,8 +3,6 @@ namespace Polyfills; using System; using System.Text; -using System.IO; -using System.Runtime.CompilerServices; static partial class Polyfill { /// diff --git a/src/Split/net11.0/Polyfill.cs b/src/Split/net11.0/Polyfill.cs index 16e916262..b7b21af17 100644 --- a/src/Split/net11.0/Polyfill.cs +++ b/src/Split/net11.0/Polyfill.cs @@ -17,5 +17,5 @@ namespace Polyfills; #endif static partial class Polyfill { - const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond * 1000; + const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; } diff --git a/src/Split/net461/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs b/src/Split/net461/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs index 32b0afded..8bad1d83d 100644 --- a/src/Split/net461/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs +++ b/src/Split/net461/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs @@ -15,7 +15,7 @@ public static void ThrowIfZero(T value, [CallerArgumentExpression(nameof(valu { if (value.Equals(default)) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } /// Throws an if is zero. @@ -23,12 +23,12 @@ public static void ThrowIfZero(nint value, [CallerArgumentExpression(nameof(valu { if (value == 0) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } [DoesNotReturn] - static void ThrowZero(string? paramName) => - throw new ArgumentOutOfRangeException(paramName, "Value must not be zero."); + static void ThrowZero(T value, string? paramName) => + throw new ArgumentOutOfRangeException(paramName, value, $"{paramName} ('{value}') must be a non-zero value."); public static void ThrowIfNegative(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : struct, IComparable { @@ -112,7 +112,7 @@ public static void ThrowIfGreaterThanOrEqual(nint value, nint other, [CallerArgu { if (value >= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than {other}."); } } public static void ThrowIfLessThan(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) @@ -144,7 +144,7 @@ public static void ThrowIfLessThanOrEqual(nint value, nint other, [CallerArgumen { if (value <= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than {other}."); } } } diff --git a/src/Split/net461/ConvertPolyfill.cs b/src/Split/net461/ConvertPolyfill.cs index 412674710..1f45391f9 100644 --- a/src/Split/net461/ConvertPolyfill.cs +++ b/src/Split/net461/ConvertPolyfill.cs @@ -134,19 +134,19 @@ public static OperationStatus FromHexString(ReadOnlySpan source, Span= destination.Length) { charsConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal(source[i]); + var lo = GetHexVal(source[i + 1]); + if (hi < 0 || lo < 0) { - charsConsumed = i; + charsConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; @@ -193,19 +193,19 @@ public static OperationStatus FromHexString(ReadOnlySpan utf8Source, Span< var j = 0; while (i + 1 < utf8Source.Length) { - var hi = GetHexVal((char)utf8Source[i]); - var lo = GetHexVal((char)utf8Source[i + 1]); - if (hi < 0 || lo < 0) + if (j >= destination.Length) { bytesConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal((char)utf8Source[i]); + var lo = GetHexVal((char)utf8Source[i + 1]); + if (hi < 0 || lo < 0) { - bytesConsumed = i; + bytesConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; diff --git a/src/Split/net461/DateTimeOffsetPolyfill.cs b/src/Split/net461/DateTimeOffsetPolyfill.cs index 6837790e5..012b944dc 100644 --- a/src/Split/net461/DateTimeOffsetPolyfill.cs +++ b/src/Split/net461/DateTimeOffsetPolyfill.cs @@ -17,7 +17,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTimeOffset(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Offset); diff --git a/src/Split/net461/DateTimePolyfill.cs b/src/Split/net461/DateTimePolyfill.cs index 4641f3e59..430683e14 100644 --- a/src/Split/net461/DateTimePolyfill.cs +++ b/src/Split/net461/DateTimePolyfill.cs @@ -16,7 +16,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTime(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Kind); diff --git a/src/Split/net461/EqualityComparerPolyfill.cs b/src/Split/net461/EqualityComparerPolyfill.cs index e45f2a30e..55cb3ad44 100644 --- a/src/Split/net461/EqualityComparerPolyfill.cs +++ b/src/Split/net461/EqualityComparerPolyfill.cs @@ -19,8 +19,14 @@ public override int GetHashCode(T obj) => /// public static EqualityComparer Create( Func equals, - Func? getHashCode = null) => - new DelegateEqualityComparer(equals, getHashCode); + Func? getHashCode = null) + { + if (equals is null) + { + throw new ArgumentNullException(nameof(equals)); + } + return new DelegateEqualityComparer(equals, getHashCode); + } } class KeySelectorEqualityComparer(Func keySelector, IEqualityComparer? keyComparer) : EqualityComparer @@ -41,7 +47,13 @@ public override int GetHashCode(T obj) /// public static EqualityComparer Create( Func keySelector, - IEqualityComparer? keyComparer = null) => - new KeySelectorEqualityComparer(keySelector, keyComparer); + IEqualityComparer? keyComparer = null) + { + if (keySelector is null) + { + throw new ArgumentNullException(nameof(keySelector)); + } + return new KeySelectorEqualityComparer(keySelector, keyComparer); + } } } diff --git a/src/Split/net461/FileUnixModePolyfill.cs b/src/Split/net461/FileUnixModePolyfill.cs index 2440f7800..701ef07cb 100644 --- a/src/Split/net461/FileUnixModePolyfill.cs +++ b/src/Split/net461/FileUnixModePolyfill.cs @@ -47,8 +47,8 @@ public static UnixFileMode GetUnixFileMode(string path) var mode = output[0] switch { '1' => UnixFileMode.StickyBit, - '2' => UnixFileMode.SetUser, - '4' => UnixFileMode.SetGroup, + '2' => UnixFileMode.SetGroup, + '4' => UnixFileMode.SetUser, _ => UnixFileMode.None }; mode = output[1] switch @@ -158,13 +158,13 @@ public static void SetUnixFileMode(string path, UnixFileMode mode) octal[3] += 1; break; case "setuser": - octal[0] += 2; + octal[0] += 4; break; case "stickybit": octal[0] += 1; break; case "setgroup": - octal[0] += 4; + octal[0] += 2; break; default: throw new("Invalid notation detected"); diff --git a/src/Split/net461/Numbers/DoublePolyfill.cs b/src/Split/net461/Numbers/DoublePolyfill.cs index ba5333f69..2e3e40efd 100644 --- a/src/Split/net461/Numbers/DoublePolyfill.cs +++ b/src/Split/net461/Numbers/DoublePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out double result) => - double.TryParse(s, NumberStyles.Float, provider, out result); + double.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,7 +28,7 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its double-precision floating-point number equivalent.. /// public static bool TryParse(ReadOnlySpan utf8Text, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Converts the span representation of a number in a specified style and culture-specific format to its double-precision floating-point number equivalent. A return value indicates whether the conversion succeeded or failed. /// @@ -43,7 +43,7 @@ public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatPro /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out double result) => - double.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + double.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/net461/Numbers/SinglePolyfill.cs b/src/Split/net461/Numbers/SinglePolyfill.cs index 69af78095..2f6e02a82 100644 --- a/src/Split/net461/Numbers/SinglePolyfill.cs +++ b/src/Split/net461/Numbers/SinglePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out float result) => - float.TryParse(s, NumberStyles.Float, provider, out result); + float.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,7 +28,7 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its single-precision floating-point number equivalent. /// public static bool TryParse(ReadOnlySpan utf8Text, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Converts the span representation of a number in a specified style and culture-specific format to its single-precision floating-point number equivalent. A return value indicates whether the conversion succeeded or failed. /// @@ -43,7 +43,7 @@ public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatPro /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out float result) => - float.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + float.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/net461/Polyfill.cs b/src/Split/net461/Polyfill.cs index 16e916262..b7b21af17 100644 --- a/src/Split/net461/Polyfill.cs +++ b/src/Split/net461/Polyfill.cs @@ -17,5 +17,5 @@ namespace Polyfills; #endif static partial class Polyfill { - const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond * 1000; + const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; } diff --git a/src/Split/net461/Polyfill_ArraySegment.cs b/src/Split/net461/Polyfill_ArraySegment.cs index 96e83c18d..05ea1f659 100644 --- a/src/Split/net461/Polyfill_ArraySegment.cs +++ b/src/Split/net461/Polyfill_ArraySegment.cs @@ -12,15 +12,19 @@ static partial class Polyfill /// public static void CopyTo(this ArraySegment target, ArraySegment destination) { - if (target.Count > destination.Count) + if (target.Array == null) { - throw new ArgumentException("DestinationTooShort", nameof(destination)); + throw new InvalidOperationException("InvalidOperation_NullArray"); } - if (target.Array == null) + if (destination.Array == null) { throw new InvalidOperationException("InvalidOperation_NullArray"); } - Array.Copy(target.Array!, target.Offset, destination.Array, destination.Offset, target.Count); + if (target.Count > destination.Count) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } + Array.Copy(target.Array, target.Offset, destination.Array, destination.Offset, target.Count); } /// /// Copies the contents of this instance into the specified destination array of the same type T. diff --git a/src/Split/net461/Polyfill_ConcurrentDictionary.cs b/src/Split/net461/Polyfill_ConcurrentDictionary.cs index 8805de34b..a3547dd29 100644 --- a/src/Split/net461/Polyfill_ConcurrentDictionary.cs +++ b/src/Split/net461/Polyfill_ConcurrentDictionary.cs @@ -12,6 +12,10 @@ static partial class Polyfill public static TValue GetOrAdd(this ConcurrentDictionary target, TKey key, Func valueFactory, TArg factoryArgument) where TKey : notnull { + if (valueFactory == null) + { + throw new ArgumentNullException(nameof(valueFactory)); + } while (true) { TValue value; diff --git a/src/Split/net461/Polyfill_Encoding_GetChars.cs b/src/Split/net461/Polyfill_Encoding_GetChars.cs index f5a7de80b..4b956e845 100644 --- a/src/Split/net461/Polyfill_Encoding_GetChars.cs +++ b/src/Split/net461/Polyfill_Encoding_GetChars.cs @@ -25,7 +25,7 @@ public static int GetChars(this Encoding target, ReadOnlySpan bytes, Span< var charArray = new char[bytes.Length]; var array = bytes.ToArray(); var count = target.GetChars(array, 0, bytes.Length, charArray, 0); - new ReadOnlySpan(charArray).CopyTo(chars); + new ReadOnlySpan(charArray, 0, count).CopyTo(chars); return count; } #endif diff --git a/src/Split/net461/Polyfill_List.cs b/src/Split/net461/Polyfill_List.cs index c2e76ed77..7a9348d0c 100644 --- a/src/Split/net461/Polyfill_List.cs +++ b/src/Split/net461/Polyfill_List.cs @@ -21,6 +21,10 @@ public static void AddRange(this List target, ReadOnlySpan source) /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { + if ((uint)index > (uint)target.Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } for (var i = 0; i < source.Length; i++) { var item = source[i]; @@ -30,6 +34,10 @@ public static void InsertRange(this List target, int index, ReadOnlySpanCopies the entire to a span. public static void CopyTo(this List target, Span destination) { + if (target.Count > destination.Length) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } for (var index = 0; index < target.Count; index++) { destination[index] = target[index]; diff --git a/src/Split/net461/Polyfill_Memory_SpanSort.cs b/src/Split/net461/Polyfill_Memory_SpanSort.cs index acc5c8666..b30957972 100644 --- a/src/Split/net461/Polyfill_Memory_SpanSort.cs +++ b/src/Split/net461/Polyfill_Memory_SpanSort.cs @@ -77,12 +77,8 @@ internal ComparerWrapper(Comparison comparison) throw new ArgumentNullException(nameof(comparison)); this.comparison = comparison; } - public int Compare(T? x, T? y) - { - if (x is null || y is null) - return 0; - return comparison((T)x, (T)y); - } + public int Compare(T? x, T? y) => + comparison(x!, y!); } } #endif diff --git a/src/Split/net461/Polyfill_MicroNanosecondAdd.cs b/src/Split/net461/Polyfill_MicroNanosecondAdd.cs index 17d2c8d00..e099e1260 100644 --- a/src/Split/net461/Polyfill_MicroNanosecondAdd.cs +++ b/src/Split/net461/Polyfill_MicroNanosecondAdd.cs @@ -8,10 +8,10 @@ static partial class Polyfill /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTime AddMicroseconds(this DateTime target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); /// /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTimeOffset AddMicroseconds(this DateTimeOffset target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); } diff --git a/src/Split/net461/Polyfill_String.cs b/src/Split/net461/Polyfill_String.cs index bab55956e..254f4be0a 100644 --- a/src/Split/net461/Polyfill_String.cs +++ b/src/Split/net461/Polyfill_String.cs @@ -3,8 +3,6 @@ namespace Polyfills; using System; using System.Text; -using System.IO; -using System.Runtime.CompilerServices; static partial class Polyfill { #if FeatureMemory @@ -147,22 +145,49 @@ public static int LastIndexOf(this string target, char value, int startIndex, in /// /// Replaces all newline sequences in the current string with . /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ReplaceLineEndings(this string target, string replacementText) { + var index = IndexOfNewlineChar(target, 0, out var stride); + if (index < 0) + { + return target; + } var builder = new StringBuilder(target.Length); - using var reader = new StringReader(target); + var position = 0; while (true) { - var line = reader.ReadLine(); - if (line == null) + builder.Append(target, position, index - position); + builder.Append(replacementText); + position = index + stride; + index = IndexOfNewlineChar(target, position, out stride); + if (index < 0) { break; } - builder.Append(line); - builder.Append(replacementText); } - return builder.ToString(0, builder.Length - replacementText.Length); + builder.Append(target, position, target.Length - position); + return builder.ToString(); + } + static int IndexOfNewlineChar(string text, int startIndex, out int stride) + { + stride = 0; + for (var index = startIndex; index < text.Length; index++) + { + switch (text[index]) + { + case '\r': + stride = index + 1 < text.Length && text[index + 1] == '\n' ? 2 : 1; + return index; + case '\n': + case '\f': + case '\u0085': + case '\u2028': + case '\u2029': + stride = 1; + return index; + } + } + return -1; } /// /// Replaces all newline sequences in the current string with . diff --git a/src/Split/net461/Polyfill_StringBuilder_Append.cs b/src/Split/net461/Polyfill_StringBuilder_Append.cs index 0b60749a9..d3970ce53 100644 --- a/src/Split/net461/Polyfill_StringBuilder_Append.cs +++ b/src/Split/net461/Polyfill_StringBuilder_Append.cs @@ -10,6 +10,14 @@ static partial class Polyfill /// public static StringBuilder Append(this StringBuilder target, StringBuilder? value, int startIndex, int count) { + if (startIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(startIndex)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } if (value == null) { if (startIndex == 0 && count == 0) diff --git a/src/Split/net461/Polyfill_StringBuilder_CopyTo.cs b/src/Split/net461/Polyfill_StringBuilder_CopyTo.cs index dc625a7bc..ba99e0a1f 100644 --- a/src/Split/net461/Polyfill_StringBuilder_CopyTo.cs +++ b/src/Split/net461/Polyfill_StringBuilder_CopyTo.cs @@ -15,20 +15,25 @@ public static void CopyTo( Span destination, int count) { - var destinationIndex = 0; - while (true) + if (count < 0) { - if (sourceIndex == target.Length) - { - break; - } - if (destinationIndex == count) - { - break; - } - destination[destinationIndex] = target[sourceIndex]; - destinationIndex++; - sourceIndex++; + throw new ArgumentOutOfRangeException(nameof(count)); + } + if ((uint)sourceIndex > (uint)target.Length) + { + throw new ArgumentOutOfRangeException(nameof(sourceIndex)); + } + if (sourceIndex > target.Length - count) + { + throw new ArgumentException("The source StringBuilder does not contain enough characters from sourceIndex onward to satisfy count."); + } + if (count > destination.Length) + { + throw new ArgumentException("The destination span is too short to hold the requested characters.", nameof(destination)); + } + for (var index = 0; index < count; index++) + { + destination[index] = target[sourceIndex + index]; } } } diff --git a/src/Split/net461/Polyfill_Type.cs b/src/Split/net461/Polyfill_Type.cs index 7890e2168..b99bdfab5 100644 --- a/src/Split/net461/Polyfill_Type.cs +++ b/src/Split/net461/Polyfill_Type.cs @@ -7,9 +7,15 @@ namespace Polyfills; using System.Linq; static partial class Polyfill { - public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInfo other) => - target.MetadataToken == other.MetadataToken && - target.Module.Equals(other.Module); + public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInfo other) + { + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } + return target.MetadataToken == other.MetadataToken && + target.Module.Equals(other.Module); + } /// /// Searches for the specified method whose parameters match the specified generic parameter count, argument types and modifiers, using the specified binding constraints. /// @@ -22,6 +28,25 @@ public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInf Type[] types, ParameterModifier[]? modifiers) { + if (name is null) + { + throw new ArgumentNullException(nameof(name)); + } + if (genericParameterCount < 0) + { + throw new ArgumentOutOfRangeException(nameof(genericParameterCount)); + } + if (types is null) + { + throw new ArgumentNullException(nameof(types)); + } + foreach (var type in types) + { + if (type is null) + { + throw new ArgumentNullException(nameof(types)); + } + } var methods = target.GetMethods(bindingAttr); if (genericParameterCount == 0) { diff --git a/src/Split/net461/RandomNumberGeneratorPolyfill.cs b/src/Split/net461/RandomNumberGeneratorPolyfill.cs index 08384a7ea..539aa7aba 100644 --- a/src/Split/net461/RandomNumberGeneratorPolyfill.cs +++ b/src/Split/net461/RandomNumberGeneratorPolyfill.cs @@ -34,7 +34,7 @@ public static int GetInt32(int fromInclusive, int toExclusive) { generator.GetBytes(bytes); value = BitConverter.ToUInt32(bytes, 0) & mask; - } while (value >= range); + } while (value > range); return (int) (fromInclusive + value); } /// diff --git a/src/Split/net461/StringPolyfill.cs b/src/Split/net461/StringPolyfill.cs index bbdee2733..b14512f1d 100644 --- a/src/Split/net461/StringPolyfill.cs +++ b/src/Split/net461/StringPolyfill.cs @@ -50,6 +50,10 @@ public static string Join(char separator, scoped ReadOnlySpan values) span = span.Slice(1); } var value = values[index]; + if (value is null) + { + continue; + } value.CopyTo(span); span = span.Slice(value.Length); } diff --git a/src/Split/net461/TaskCompletionSource.cs b/src/Split/net461/TaskCompletionSource.cs index 04b507f76..67a35f938 100644 --- a/src/Split/net461/TaskCompletionSource.cs +++ b/src/Split/net461/TaskCompletionSource.cs @@ -77,5 +77,5 @@ public TaskCompletionSource(object? state, TaskCreationOptions creationOptions) /// /// Attempts to transition the underlying into the state. /// - public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(default); + public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(cancellationToken); } diff --git a/src/Split/net461/TimeSpanPolyfill.cs b/src/Split/net461/TimeSpanPolyfill.cs index 94d13cbbb..59d7c7974 100644 --- a/src/Split/net461/TimeSpanPolyfill.cs +++ b/src/Split/net461/TimeSpanPolyfill.cs @@ -15,7 +15,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microseconds => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new TimeSpan(target.Days, target.Hours, target.Minutes, 0); @@ -24,52 +24,71 @@ long TicksComponent() } } const long MicrosecondsToTicks = TimeSpan.TicksPerMillisecond / 1000; + const long MicrosecondsPerMillisecond = 1_000; + const long MicrosecondsPerSecond = 1_000_000; + const long MicrosecondsPerMinute = 60_000_000; + const long MicrosecondsPerHour = 3_600_000_000; + const long MicrosecondsPerDay = 86_400_000_000; + static TimeSpan TicksFromMicroseconds(decimal totalMicroseconds) + { + var ticks = totalMicroseconds * MicrosecondsToTicks; + if (ticks < long.MinValue || ticks > long.MaxValue) + { + throw new ArgumentOutOfRangeException(null, "TimeSpan overflowed because the duration is too long."); + } + return new((long)ticks); + } extension(TimeSpan) { /// /// Initializes a new instance of the structure to a specified number of days, hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromDays(int days, int hours = 0, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)days * TimeSpan.TicksPerDay + - (long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)days * MicrosecondsPerDay + + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromHours(int hours, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromMinutes(int minutes, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of seconds, milliseconds, and microseconds. /// public static TimeSpan FromSeconds(long seconds, long milliseconds = 0, long microseconds = 0) => - new(seconds * TimeSpan.TicksPerSecond + - milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of milliseconds and microseconds. /// public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0) => - new(milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of microseconds. /// public static TimeSpan FromMicroseconds(long microseconds) => - new(microseconds * MicrosecondsToTicks); + TicksFromMicroseconds(microseconds); } } diff --git a/src/Split/net461/XDocumentPolyfill.cs b/src/Split/net461/XDocumentPolyfill.cs index 1611acf19..876842223 100644 --- a/src/Split/net461/XDocumentPolyfill.cs +++ b/src/Split/net461/XDocumentPolyfill.cs @@ -25,12 +25,10 @@ static partial class XDocumentPolyfill /// /// Asynchronously creates a new XDocument and initializes its underlying XML tree using the specified stream, optionally preserving white space. /// - public static async Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) + public static Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - using var reader = new StreamReader(stream); - var content = await reader.ReadToEndAsync(); - return XDocument.Parse(content, options); + return Task.FromResult(XDocument.Load(stream, options)); } /// /// Asynchronously creates a new XDocument and initializes its underlying XML tree using the specified text reader, optionally preserving white space. diff --git a/src/Split/net461/XElementPolyfill.cs b/src/Split/net461/XElementPolyfill.cs index 27208879a..9347d9cdd 100644 --- a/src/Split/net461/XElementPolyfill.cs +++ b/src/Split/net461/XElementPolyfill.cs @@ -25,12 +25,10 @@ static partial class XElementPolyfill /// /// Asynchronously creates a new XElement and initializes its underlying XML tree using the specified stream, optionally preserving white space. /// - public static async Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) + public static Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - using var reader = new StreamReader(stream); - var content = await reader.ReadToEndAsync(); - return XElement.Parse(content, options); + return Task.FromResult(XElement.Load(stream, options)); } /// /// Asynchronously creates a new XElement and initializes its underlying XML tree using the specified text reader, optionally preserving white space. diff --git a/src/Split/net461/ZipFilePolyfill.cs b/src/Split/net461/ZipFilePolyfill.cs index 6cb361e6f..d908c5633 100644 --- a/src/Split/net461/ZipFilePolyfill.cs +++ b/src/Split/net461/ZipFilePolyfill.cs @@ -53,11 +53,12 @@ static void ExtractToDirectoryPolyfill( string destinationDirectory, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + return; } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, null); } static void ExtractToDirectoryPolyfill( string sourceArchiveFile, @@ -65,11 +66,43 @@ static void ExtractToDirectoryPolyfill( Encoding encoding, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); + return; + } + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, encoding); + } + static void ExtractToDirectoryWithOverwrite( + string sourceArchiveFile, + string destinationDirectory, + Encoding? encoding) + { + var destinationRoot = Path.GetFullPath(destinationDirectory); + Directory.CreateDirectory(destinationRoot); + var normalizedRoot = destinationRoot; + if (normalizedRoot[normalizedRoot.Length - 1] != Path.DirectorySeparatorChar) + { + normalizedRoot += Path.DirectorySeparatorChar; + } + using var archive = encoding == null + ? ZipFile.OpenRead(sourceArchiveFile) + : ZipFile.Open(sourceArchiveFile, ZipArchiveMode.Read, encoding); + foreach (var entry in archive.Entries) + { + var destinationPath = Path.GetFullPath(Path.Combine(destinationRoot, entry.FullName)); + if (!destinationPath.StartsWith(normalizedRoot, StringComparison.Ordinal)) + { + throw new IOException("Extracting Zip entry would have resulted in a file outside the specified destination directory."); + } + if (entry.Name.Length == 0) + { + Directory.CreateDirectory(destinationPath); + continue; + } + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + entry.ExtractToFile(destinationPath, overwrite: true); } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); } /// /// Asynchronously opens a ZipArchive on the specified archiveFileName in the specified ZipArchiveMode mode. diff --git a/src/Split/net462/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs b/src/Split/net462/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs index 32b0afded..8bad1d83d 100644 --- a/src/Split/net462/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs +++ b/src/Split/net462/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs @@ -15,7 +15,7 @@ public static void ThrowIfZero(T value, [CallerArgumentExpression(nameof(valu { if (value.Equals(default)) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } /// Throws an if is zero. @@ -23,12 +23,12 @@ public static void ThrowIfZero(nint value, [CallerArgumentExpression(nameof(valu { if (value == 0) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } [DoesNotReturn] - static void ThrowZero(string? paramName) => - throw new ArgumentOutOfRangeException(paramName, "Value must not be zero."); + static void ThrowZero(T value, string? paramName) => + throw new ArgumentOutOfRangeException(paramName, value, $"{paramName} ('{value}') must be a non-zero value."); public static void ThrowIfNegative(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : struct, IComparable { @@ -112,7 +112,7 @@ public static void ThrowIfGreaterThanOrEqual(nint value, nint other, [CallerArgu { if (value >= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than {other}."); } } public static void ThrowIfLessThan(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) @@ -144,7 +144,7 @@ public static void ThrowIfLessThanOrEqual(nint value, nint other, [CallerArgumen { if (value <= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than {other}."); } } } diff --git a/src/Split/net462/ConvertPolyfill.cs b/src/Split/net462/ConvertPolyfill.cs index 412674710..1f45391f9 100644 --- a/src/Split/net462/ConvertPolyfill.cs +++ b/src/Split/net462/ConvertPolyfill.cs @@ -134,19 +134,19 @@ public static OperationStatus FromHexString(ReadOnlySpan source, Span= destination.Length) { charsConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal(source[i]); + var lo = GetHexVal(source[i + 1]); + if (hi < 0 || lo < 0) { - charsConsumed = i; + charsConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; @@ -193,19 +193,19 @@ public static OperationStatus FromHexString(ReadOnlySpan utf8Source, Span< var j = 0; while (i + 1 < utf8Source.Length) { - var hi = GetHexVal((char)utf8Source[i]); - var lo = GetHexVal((char)utf8Source[i + 1]); - if (hi < 0 || lo < 0) + if (j >= destination.Length) { bytesConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal((char)utf8Source[i]); + var lo = GetHexVal((char)utf8Source[i + 1]); + if (hi < 0 || lo < 0) { - bytesConsumed = i; + bytesConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; diff --git a/src/Split/net462/DateTimeOffsetPolyfill.cs b/src/Split/net462/DateTimeOffsetPolyfill.cs index 6837790e5..012b944dc 100644 --- a/src/Split/net462/DateTimeOffsetPolyfill.cs +++ b/src/Split/net462/DateTimeOffsetPolyfill.cs @@ -17,7 +17,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTimeOffset(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Offset); diff --git a/src/Split/net462/DateTimePolyfill.cs b/src/Split/net462/DateTimePolyfill.cs index 4641f3e59..430683e14 100644 --- a/src/Split/net462/DateTimePolyfill.cs +++ b/src/Split/net462/DateTimePolyfill.cs @@ -16,7 +16,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTime(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Kind); diff --git a/src/Split/net462/EqualityComparerPolyfill.cs b/src/Split/net462/EqualityComparerPolyfill.cs index e45f2a30e..55cb3ad44 100644 --- a/src/Split/net462/EqualityComparerPolyfill.cs +++ b/src/Split/net462/EqualityComparerPolyfill.cs @@ -19,8 +19,14 @@ public override int GetHashCode(T obj) => /// public static EqualityComparer Create( Func equals, - Func? getHashCode = null) => - new DelegateEqualityComparer(equals, getHashCode); + Func? getHashCode = null) + { + if (equals is null) + { + throw new ArgumentNullException(nameof(equals)); + } + return new DelegateEqualityComparer(equals, getHashCode); + } } class KeySelectorEqualityComparer(Func keySelector, IEqualityComparer? keyComparer) : EqualityComparer @@ -41,7 +47,13 @@ public override int GetHashCode(T obj) /// public static EqualityComparer Create( Func keySelector, - IEqualityComparer? keyComparer = null) => - new KeySelectorEqualityComparer(keySelector, keyComparer); + IEqualityComparer? keyComparer = null) + { + if (keySelector is null) + { + throw new ArgumentNullException(nameof(keySelector)); + } + return new KeySelectorEqualityComparer(keySelector, keyComparer); + } } } diff --git a/src/Split/net462/FileUnixModePolyfill.cs b/src/Split/net462/FileUnixModePolyfill.cs index 2440f7800..701ef07cb 100644 --- a/src/Split/net462/FileUnixModePolyfill.cs +++ b/src/Split/net462/FileUnixModePolyfill.cs @@ -47,8 +47,8 @@ public static UnixFileMode GetUnixFileMode(string path) var mode = output[0] switch { '1' => UnixFileMode.StickyBit, - '2' => UnixFileMode.SetUser, - '4' => UnixFileMode.SetGroup, + '2' => UnixFileMode.SetGroup, + '4' => UnixFileMode.SetUser, _ => UnixFileMode.None }; mode = output[1] switch @@ -158,13 +158,13 @@ public static void SetUnixFileMode(string path, UnixFileMode mode) octal[3] += 1; break; case "setuser": - octal[0] += 2; + octal[0] += 4; break; case "stickybit": octal[0] += 1; break; case "setgroup": - octal[0] += 4; + octal[0] += 2; break; default: throw new("Invalid notation detected"); diff --git a/src/Split/net462/Numbers/DoublePolyfill.cs b/src/Split/net462/Numbers/DoublePolyfill.cs index ba5333f69..2e3e40efd 100644 --- a/src/Split/net462/Numbers/DoublePolyfill.cs +++ b/src/Split/net462/Numbers/DoublePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out double result) => - double.TryParse(s, NumberStyles.Float, provider, out result); + double.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,7 +28,7 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its double-precision floating-point number equivalent.. /// public static bool TryParse(ReadOnlySpan utf8Text, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Converts the span representation of a number in a specified style and culture-specific format to its double-precision floating-point number equivalent. A return value indicates whether the conversion succeeded or failed. /// @@ -43,7 +43,7 @@ public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatPro /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out double result) => - double.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + double.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/net462/Numbers/SinglePolyfill.cs b/src/Split/net462/Numbers/SinglePolyfill.cs index 69af78095..2f6e02a82 100644 --- a/src/Split/net462/Numbers/SinglePolyfill.cs +++ b/src/Split/net462/Numbers/SinglePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out float result) => - float.TryParse(s, NumberStyles.Float, provider, out result); + float.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,7 +28,7 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its single-precision floating-point number equivalent. /// public static bool TryParse(ReadOnlySpan utf8Text, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Converts the span representation of a number in a specified style and culture-specific format to its single-precision floating-point number equivalent. A return value indicates whether the conversion succeeded or failed. /// @@ -43,7 +43,7 @@ public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatPro /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out float result) => - float.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + float.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/net462/Polyfill.cs b/src/Split/net462/Polyfill.cs index 16e916262..b7b21af17 100644 --- a/src/Split/net462/Polyfill.cs +++ b/src/Split/net462/Polyfill.cs @@ -17,5 +17,5 @@ namespace Polyfills; #endif static partial class Polyfill { - const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond * 1000; + const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; } diff --git a/src/Split/net462/Polyfill_ArraySegment.cs b/src/Split/net462/Polyfill_ArraySegment.cs index 96e83c18d..05ea1f659 100644 --- a/src/Split/net462/Polyfill_ArraySegment.cs +++ b/src/Split/net462/Polyfill_ArraySegment.cs @@ -12,15 +12,19 @@ static partial class Polyfill /// public static void CopyTo(this ArraySegment target, ArraySegment destination) { - if (target.Count > destination.Count) + if (target.Array == null) { - throw new ArgumentException("DestinationTooShort", nameof(destination)); + throw new InvalidOperationException("InvalidOperation_NullArray"); } - if (target.Array == null) + if (destination.Array == null) { throw new InvalidOperationException("InvalidOperation_NullArray"); } - Array.Copy(target.Array!, target.Offset, destination.Array, destination.Offset, target.Count); + if (target.Count > destination.Count) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } + Array.Copy(target.Array, target.Offset, destination.Array, destination.Offset, target.Count); } /// /// Copies the contents of this instance into the specified destination array of the same type T. diff --git a/src/Split/net462/Polyfill_ConcurrentDictionary.cs b/src/Split/net462/Polyfill_ConcurrentDictionary.cs index 8805de34b..a3547dd29 100644 --- a/src/Split/net462/Polyfill_ConcurrentDictionary.cs +++ b/src/Split/net462/Polyfill_ConcurrentDictionary.cs @@ -12,6 +12,10 @@ static partial class Polyfill public static TValue GetOrAdd(this ConcurrentDictionary target, TKey key, Func valueFactory, TArg factoryArgument) where TKey : notnull { + if (valueFactory == null) + { + throw new ArgumentNullException(nameof(valueFactory)); + } while (true) { TValue value; diff --git a/src/Split/net462/Polyfill_Encoding_GetChars.cs b/src/Split/net462/Polyfill_Encoding_GetChars.cs index f5a7de80b..4b956e845 100644 --- a/src/Split/net462/Polyfill_Encoding_GetChars.cs +++ b/src/Split/net462/Polyfill_Encoding_GetChars.cs @@ -25,7 +25,7 @@ public static int GetChars(this Encoding target, ReadOnlySpan bytes, Span< var charArray = new char[bytes.Length]; var array = bytes.ToArray(); var count = target.GetChars(array, 0, bytes.Length, charArray, 0); - new ReadOnlySpan(charArray).CopyTo(chars); + new ReadOnlySpan(charArray, 0, count).CopyTo(chars); return count; } #endif diff --git a/src/Split/net462/Polyfill_List.cs b/src/Split/net462/Polyfill_List.cs index c2e76ed77..7a9348d0c 100644 --- a/src/Split/net462/Polyfill_List.cs +++ b/src/Split/net462/Polyfill_List.cs @@ -21,6 +21,10 @@ public static void AddRange(this List target, ReadOnlySpan source) /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { + if ((uint)index > (uint)target.Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } for (var i = 0; i < source.Length; i++) { var item = source[i]; @@ -30,6 +34,10 @@ public static void InsertRange(this List target, int index, ReadOnlySpanCopies the entire to a span. public static void CopyTo(this List target, Span destination) { + if (target.Count > destination.Length) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } for (var index = 0; index < target.Count; index++) { destination[index] = target[index]; diff --git a/src/Split/net462/Polyfill_Memory_SpanSort.cs b/src/Split/net462/Polyfill_Memory_SpanSort.cs index acc5c8666..b30957972 100644 --- a/src/Split/net462/Polyfill_Memory_SpanSort.cs +++ b/src/Split/net462/Polyfill_Memory_SpanSort.cs @@ -77,12 +77,8 @@ internal ComparerWrapper(Comparison comparison) throw new ArgumentNullException(nameof(comparison)); this.comparison = comparison; } - public int Compare(T? x, T? y) - { - if (x is null || y is null) - return 0; - return comparison((T)x, (T)y); - } + public int Compare(T? x, T? y) => + comparison(x!, y!); } } #endif diff --git a/src/Split/net462/Polyfill_MicroNanosecondAdd.cs b/src/Split/net462/Polyfill_MicroNanosecondAdd.cs index 17d2c8d00..e099e1260 100644 --- a/src/Split/net462/Polyfill_MicroNanosecondAdd.cs +++ b/src/Split/net462/Polyfill_MicroNanosecondAdd.cs @@ -8,10 +8,10 @@ static partial class Polyfill /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTime AddMicroseconds(this DateTime target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); /// /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTimeOffset AddMicroseconds(this DateTimeOffset target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); } diff --git a/src/Split/net462/Polyfill_String.cs b/src/Split/net462/Polyfill_String.cs index bab55956e..254f4be0a 100644 --- a/src/Split/net462/Polyfill_String.cs +++ b/src/Split/net462/Polyfill_String.cs @@ -3,8 +3,6 @@ namespace Polyfills; using System; using System.Text; -using System.IO; -using System.Runtime.CompilerServices; static partial class Polyfill { #if FeatureMemory @@ -147,22 +145,49 @@ public static int LastIndexOf(this string target, char value, int startIndex, in /// /// Replaces all newline sequences in the current string with . /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ReplaceLineEndings(this string target, string replacementText) { + var index = IndexOfNewlineChar(target, 0, out var stride); + if (index < 0) + { + return target; + } var builder = new StringBuilder(target.Length); - using var reader = new StringReader(target); + var position = 0; while (true) { - var line = reader.ReadLine(); - if (line == null) + builder.Append(target, position, index - position); + builder.Append(replacementText); + position = index + stride; + index = IndexOfNewlineChar(target, position, out stride); + if (index < 0) { break; } - builder.Append(line); - builder.Append(replacementText); } - return builder.ToString(0, builder.Length - replacementText.Length); + builder.Append(target, position, target.Length - position); + return builder.ToString(); + } + static int IndexOfNewlineChar(string text, int startIndex, out int stride) + { + stride = 0; + for (var index = startIndex; index < text.Length; index++) + { + switch (text[index]) + { + case '\r': + stride = index + 1 < text.Length && text[index + 1] == '\n' ? 2 : 1; + return index; + case '\n': + case '\f': + case '\u0085': + case '\u2028': + case '\u2029': + stride = 1; + return index; + } + } + return -1; } /// /// Replaces all newline sequences in the current string with . diff --git a/src/Split/net462/Polyfill_StringBuilder_Append.cs b/src/Split/net462/Polyfill_StringBuilder_Append.cs index 0b60749a9..d3970ce53 100644 --- a/src/Split/net462/Polyfill_StringBuilder_Append.cs +++ b/src/Split/net462/Polyfill_StringBuilder_Append.cs @@ -10,6 +10,14 @@ static partial class Polyfill /// public static StringBuilder Append(this StringBuilder target, StringBuilder? value, int startIndex, int count) { + if (startIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(startIndex)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } if (value == null) { if (startIndex == 0 && count == 0) diff --git a/src/Split/net462/Polyfill_StringBuilder_CopyTo.cs b/src/Split/net462/Polyfill_StringBuilder_CopyTo.cs index dc625a7bc..ba99e0a1f 100644 --- a/src/Split/net462/Polyfill_StringBuilder_CopyTo.cs +++ b/src/Split/net462/Polyfill_StringBuilder_CopyTo.cs @@ -15,20 +15,25 @@ public static void CopyTo( Span destination, int count) { - var destinationIndex = 0; - while (true) + if (count < 0) { - if (sourceIndex == target.Length) - { - break; - } - if (destinationIndex == count) - { - break; - } - destination[destinationIndex] = target[sourceIndex]; - destinationIndex++; - sourceIndex++; + throw new ArgumentOutOfRangeException(nameof(count)); + } + if ((uint)sourceIndex > (uint)target.Length) + { + throw new ArgumentOutOfRangeException(nameof(sourceIndex)); + } + if (sourceIndex > target.Length - count) + { + throw new ArgumentException("The source StringBuilder does not contain enough characters from sourceIndex onward to satisfy count."); + } + if (count > destination.Length) + { + throw new ArgumentException("The destination span is too short to hold the requested characters.", nameof(destination)); + } + for (var index = 0; index < count; index++) + { + destination[index] = target[sourceIndex + index]; } } } diff --git a/src/Split/net462/Polyfill_Type.cs b/src/Split/net462/Polyfill_Type.cs index 7890e2168..b99bdfab5 100644 --- a/src/Split/net462/Polyfill_Type.cs +++ b/src/Split/net462/Polyfill_Type.cs @@ -7,9 +7,15 @@ namespace Polyfills; using System.Linq; static partial class Polyfill { - public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInfo other) => - target.MetadataToken == other.MetadataToken && - target.Module.Equals(other.Module); + public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInfo other) + { + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } + return target.MetadataToken == other.MetadataToken && + target.Module.Equals(other.Module); + } /// /// Searches for the specified method whose parameters match the specified generic parameter count, argument types and modifiers, using the specified binding constraints. /// @@ -22,6 +28,25 @@ public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInf Type[] types, ParameterModifier[]? modifiers) { + if (name is null) + { + throw new ArgumentNullException(nameof(name)); + } + if (genericParameterCount < 0) + { + throw new ArgumentOutOfRangeException(nameof(genericParameterCount)); + } + if (types is null) + { + throw new ArgumentNullException(nameof(types)); + } + foreach (var type in types) + { + if (type is null) + { + throw new ArgumentNullException(nameof(types)); + } + } var methods = target.GetMethods(bindingAttr); if (genericParameterCount == 0) { diff --git a/src/Split/net462/RandomNumberGeneratorPolyfill.cs b/src/Split/net462/RandomNumberGeneratorPolyfill.cs index 08384a7ea..539aa7aba 100644 --- a/src/Split/net462/RandomNumberGeneratorPolyfill.cs +++ b/src/Split/net462/RandomNumberGeneratorPolyfill.cs @@ -34,7 +34,7 @@ public static int GetInt32(int fromInclusive, int toExclusive) { generator.GetBytes(bytes); value = BitConverter.ToUInt32(bytes, 0) & mask; - } while (value >= range); + } while (value > range); return (int) (fromInclusive + value); } /// diff --git a/src/Split/net462/StringPolyfill.cs b/src/Split/net462/StringPolyfill.cs index bbdee2733..b14512f1d 100644 --- a/src/Split/net462/StringPolyfill.cs +++ b/src/Split/net462/StringPolyfill.cs @@ -50,6 +50,10 @@ public static string Join(char separator, scoped ReadOnlySpan values) span = span.Slice(1); } var value = values[index]; + if (value is null) + { + continue; + } value.CopyTo(span); span = span.Slice(value.Length); } diff --git a/src/Split/net462/TaskCompletionSource.cs b/src/Split/net462/TaskCompletionSource.cs index 04b507f76..67a35f938 100644 --- a/src/Split/net462/TaskCompletionSource.cs +++ b/src/Split/net462/TaskCompletionSource.cs @@ -77,5 +77,5 @@ public TaskCompletionSource(object? state, TaskCreationOptions creationOptions) /// /// Attempts to transition the underlying into the state. /// - public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(default); + public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(cancellationToken); } diff --git a/src/Split/net462/TimeSpanPolyfill.cs b/src/Split/net462/TimeSpanPolyfill.cs index 94d13cbbb..59d7c7974 100644 --- a/src/Split/net462/TimeSpanPolyfill.cs +++ b/src/Split/net462/TimeSpanPolyfill.cs @@ -15,7 +15,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microseconds => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new TimeSpan(target.Days, target.Hours, target.Minutes, 0); @@ -24,52 +24,71 @@ long TicksComponent() } } const long MicrosecondsToTicks = TimeSpan.TicksPerMillisecond / 1000; + const long MicrosecondsPerMillisecond = 1_000; + const long MicrosecondsPerSecond = 1_000_000; + const long MicrosecondsPerMinute = 60_000_000; + const long MicrosecondsPerHour = 3_600_000_000; + const long MicrosecondsPerDay = 86_400_000_000; + static TimeSpan TicksFromMicroseconds(decimal totalMicroseconds) + { + var ticks = totalMicroseconds * MicrosecondsToTicks; + if (ticks < long.MinValue || ticks > long.MaxValue) + { + throw new ArgumentOutOfRangeException(null, "TimeSpan overflowed because the duration is too long."); + } + return new((long)ticks); + } extension(TimeSpan) { /// /// Initializes a new instance of the structure to a specified number of days, hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromDays(int days, int hours = 0, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)days * TimeSpan.TicksPerDay + - (long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)days * MicrosecondsPerDay + + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromHours(int hours, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromMinutes(int minutes, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of seconds, milliseconds, and microseconds. /// public static TimeSpan FromSeconds(long seconds, long milliseconds = 0, long microseconds = 0) => - new(seconds * TimeSpan.TicksPerSecond + - milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of milliseconds and microseconds. /// public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0) => - new(milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of microseconds. /// public static TimeSpan FromMicroseconds(long microseconds) => - new(microseconds * MicrosecondsToTicks); + TicksFromMicroseconds(microseconds); } } diff --git a/src/Split/net462/XDocumentPolyfill.cs b/src/Split/net462/XDocumentPolyfill.cs index 1611acf19..876842223 100644 --- a/src/Split/net462/XDocumentPolyfill.cs +++ b/src/Split/net462/XDocumentPolyfill.cs @@ -25,12 +25,10 @@ static partial class XDocumentPolyfill /// /// Asynchronously creates a new XDocument and initializes its underlying XML tree using the specified stream, optionally preserving white space. /// - public static async Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) + public static Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - using var reader = new StreamReader(stream); - var content = await reader.ReadToEndAsync(); - return XDocument.Parse(content, options); + return Task.FromResult(XDocument.Load(stream, options)); } /// /// Asynchronously creates a new XDocument and initializes its underlying XML tree using the specified text reader, optionally preserving white space. diff --git a/src/Split/net462/XElementPolyfill.cs b/src/Split/net462/XElementPolyfill.cs index 27208879a..9347d9cdd 100644 --- a/src/Split/net462/XElementPolyfill.cs +++ b/src/Split/net462/XElementPolyfill.cs @@ -25,12 +25,10 @@ static partial class XElementPolyfill /// /// Asynchronously creates a new XElement and initializes its underlying XML tree using the specified stream, optionally preserving white space. /// - public static async Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) + public static Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - using var reader = new StreamReader(stream); - var content = await reader.ReadToEndAsync(); - return XElement.Parse(content, options); + return Task.FromResult(XElement.Load(stream, options)); } /// /// Asynchronously creates a new XElement and initializes its underlying XML tree using the specified text reader, optionally preserving white space. diff --git a/src/Split/net462/ZipFilePolyfill.cs b/src/Split/net462/ZipFilePolyfill.cs index 6cb361e6f..d908c5633 100644 --- a/src/Split/net462/ZipFilePolyfill.cs +++ b/src/Split/net462/ZipFilePolyfill.cs @@ -53,11 +53,12 @@ static void ExtractToDirectoryPolyfill( string destinationDirectory, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + return; } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, null); } static void ExtractToDirectoryPolyfill( string sourceArchiveFile, @@ -65,11 +66,43 @@ static void ExtractToDirectoryPolyfill( Encoding encoding, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); + return; + } + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, encoding); + } + static void ExtractToDirectoryWithOverwrite( + string sourceArchiveFile, + string destinationDirectory, + Encoding? encoding) + { + var destinationRoot = Path.GetFullPath(destinationDirectory); + Directory.CreateDirectory(destinationRoot); + var normalizedRoot = destinationRoot; + if (normalizedRoot[normalizedRoot.Length - 1] != Path.DirectorySeparatorChar) + { + normalizedRoot += Path.DirectorySeparatorChar; + } + using var archive = encoding == null + ? ZipFile.OpenRead(sourceArchiveFile) + : ZipFile.Open(sourceArchiveFile, ZipArchiveMode.Read, encoding); + foreach (var entry in archive.Entries) + { + var destinationPath = Path.GetFullPath(Path.Combine(destinationRoot, entry.FullName)); + if (!destinationPath.StartsWith(normalizedRoot, StringComparison.Ordinal)) + { + throw new IOException("Extracting Zip entry would have resulted in a file outside the specified destination directory."); + } + if (entry.Name.Length == 0) + { + Directory.CreateDirectory(destinationPath); + continue; + } + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + entry.ExtractToFile(destinationPath, overwrite: true); } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); } /// /// Asynchronously opens a ZipArchive on the specified archiveFileName in the specified ZipArchiveMode mode. diff --git a/src/Split/net47/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs b/src/Split/net47/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs index 32b0afded..8bad1d83d 100644 --- a/src/Split/net47/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs +++ b/src/Split/net47/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs @@ -15,7 +15,7 @@ public static void ThrowIfZero(T value, [CallerArgumentExpression(nameof(valu { if (value.Equals(default)) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } /// Throws an if is zero. @@ -23,12 +23,12 @@ public static void ThrowIfZero(nint value, [CallerArgumentExpression(nameof(valu { if (value == 0) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } [DoesNotReturn] - static void ThrowZero(string? paramName) => - throw new ArgumentOutOfRangeException(paramName, "Value must not be zero."); + static void ThrowZero(T value, string? paramName) => + throw new ArgumentOutOfRangeException(paramName, value, $"{paramName} ('{value}') must be a non-zero value."); public static void ThrowIfNegative(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : struct, IComparable { @@ -112,7 +112,7 @@ public static void ThrowIfGreaterThanOrEqual(nint value, nint other, [CallerArgu { if (value >= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than {other}."); } } public static void ThrowIfLessThan(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) @@ -144,7 +144,7 @@ public static void ThrowIfLessThanOrEqual(nint value, nint other, [CallerArgumen { if (value <= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than {other}."); } } } diff --git a/src/Split/net47/ConvertPolyfill.cs b/src/Split/net47/ConvertPolyfill.cs index 412674710..1f45391f9 100644 --- a/src/Split/net47/ConvertPolyfill.cs +++ b/src/Split/net47/ConvertPolyfill.cs @@ -134,19 +134,19 @@ public static OperationStatus FromHexString(ReadOnlySpan source, Span= destination.Length) { charsConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal(source[i]); + var lo = GetHexVal(source[i + 1]); + if (hi < 0 || lo < 0) { - charsConsumed = i; + charsConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; @@ -193,19 +193,19 @@ public static OperationStatus FromHexString(ReadOnlySpan utf8Source, Span< var j = 0; while (i + 1 < utf8Source.Length) { - var hi = GetHexVal((char)utf8Source[i]); - var lo = GetHexVal((char)utf8Source[i + 1]); - if (hi < 0 || lo < 0) + if (j >= destination.Length) { bytesConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal((char)utf8Source[i]); + var lo = GetHexVal((char)utf8Source[i + 1]); + if (hi < 0 || lo < 0) { - bytesConsumed = i; + bytesConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; diff --git a/src/Split/net47/DateTimeOffsetPolyfill.cs b/src/Split/net47/DateTimeOffsetPolyfill.cs index 6837790e5..012b944dc 100644 --- a/src/Split/net47/DateTimeOffsetPolyfill.cs +++ b/src/Split/net47/DateTimeOffsetPolyfill.cs @@ -17,7 +17,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTimeOffset(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Offset); diff --git a/src/Split/net47/DateTimePolyfill.cs b/src/Split/net47/DateTimePolyfill.cs index 4641f3e59..430683e14 100644 --- a/src/Split/net47/DateTimePolyfill.cs +++ b/src/Split/net47/DateTimePolyfill.cs @@ -16,7 +16,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTime(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Kind); diff --git a/src/Split/net47/EqualityComparerPolyfill.cs b/src/Split/net47/EqualityComparerPolyfill.cs index e45f2a30e..55cb3ad44 100644 --- a/src/Split/net47/EqualityComparerPolyfill.cs +++ b/src/Split/net47/EqualityComparerPolyfill.cs @@ -19,8 +19,14 @@ public override int GetHashCode(T obj) => /// public static EqualityComparer Create( Func equals, - Func? getHashCode = null) => - new DelegateEqualityComparer(equals, getHashCode); + Func? getHashCode = null) + { + if (equals is null) + { + throw new ArgumentNullException(nameof(equals)); + } + return new DelegateEqualityComparer(equals, getHashCode); + } } class KeySelectorEqualityComparer(Func keySelector, IEqualityComparer? keyComparer) : EqualityComparer @@ -41,7 +47,13 @@ public override int GetHashCode(T obj) /// public static EqualityComparer Create( Func keySelector, - IEqualityComparer? keyComparer = null) => - new KeySelectorEqualityComparer(keySelector, keyComparer); + IEqualityComparer? keyComparer = null) + { + if (keySelector is null) + { + throw new ArgumentNullException(nameof(keySelector)); + } + return new KeySelectorEqualityComparer(keySelector, keyComparer); + } } } diff --git a/src/Split/net47/FileUnixModePolyfill.cs b/src/Split/net47/FileUnixModePolyfill.cs index 2440f7800..701ef07cb 100644 --- a/src/Split/net47/FileUnixModePolyfill.cs +++ b/src/Split/net47/FileUnixModePolyfill.cs @@ -47,8 +47,8 @@ public static UnixFileMode GetUnixFileMode(string path) var mode = output[0] switch { '1' => UnixFileMode.StickyBit, - '2' => UnixFileMode.SetUser, - '4' => UnixFileMode.SetGroup, + '2' => UnixFileMode.SetGroup, + '4' => UnixFileMode.SetUser, _ => UnixFileMode.None }; mode = output[1] switch @@ -158,13 +158,13 @@ public static void SetUnixFileMode(string path, UnixFileMode mode) octal[3] += 1; break; case "setuser": - octal[0] += 2; + octal[0] += 4; break; case "stickybit": octal[0] += 1; break; case "setgroup": - octal[0] += 4; + octal[0] += 2; break; default: throw new("Invalid notation detected"); diff --git a/src/Split/net47/Numbers/DoublePolyfill.cs b/src/Split/net47/Numbers/DoublePolyfill.cs index ba5333f69..2e3e40efd 100644 --- a/src/Split/net47/Numbers/DoublePolyfill.cs +++ b/src/Split/net47/Numbers/DoublePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out double result) => - double.TryParse(s, NumberStyles.Float, provider, out result); + double.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,7 +28,7 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its double-precision floating-point number equivalent.. /// public static bool TryParse(ReadOnlySpan utf8Text, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Converts the span representation of a number in a specified style and culture-specific format to its double-precision floating-point number equivalent. A return value indicates whether the conversion succeeded or failed. /// @@ -43,7 +43,7 @@ public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatPro /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out double result) => - double.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + double.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/net47/Numbers/SinglePolyfill.cs b/src/Split/net47/Numbers/SinglePolyfill.cs index 69af78095..2f6e02a82 100644 --- a/src/Split/net47/Numbers/SinglePolyfill.cs +++ b/src/Split/net47/Numbers/SinglePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out float result) => - float.TryParse(s, NumberStyles.Float, provider, out result); + float.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,7 +28,7 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its single-precision floating-point number equivalent. /// public static bool TryParse(ReadOnlySpan utf8Text, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Converts the span representation of a number in a specified style and culture-specific format to its single-precision floating-point number equivalent. A return value indicates whether the conversion succeeded or failed. /// @@ -43,7 +43,7 @@ public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatPro /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out float result) => - float.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + float.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/net47/Polyfill.cs b/src/Split/net47/Polyfill.cs index 16e916262..b7b21af17 100644 --- a/src/Split/net47/Polyfill.cs +++ b/src/Split/net47/Polyfill.cs @@ -17,5 +17,5 @@ namespace Polyfills; #endif static partial class Polyfill { - const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond * 1000; + const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; } diff --git a/src/Split/net47/Polyfill_ArraySegment.cs b/src/Split/net47/Polyfill_ArraySegment.cs index 96e83c18d..05ea1f659 100644 --- a/src/Split/net47/Polyfill_ArraySegment.cs +++ b/src/Split/net47/Polyfill_ArraySegment.cs @@ -12,15 +12,19 @@ static partial class Polyfill /// public static void CopyTo(this ArraySegment target, ArraySegment destination) { - if (target.Count > destination.Count) + if (target.Array == null) { - throw new ArgumentException("DestinationTooShort", nameof(destination)); + throw new InvalidOperationException("InvalidOperation_NullArray"); } - if (target.Array == null) + if (destination.Array == null) { throw new InvalidOperationException("InvalidOperation_NullArray"); } - Array.Copy(target.Array!, target.Offset, destination.Array, destination.Offset, target.Count); + if (target.Count > destination.Count) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } + Array.Copy(target.Array, target.Offset, destination.Array, destination.Offset, target.Count); } /// /// Copies the contents of this instance into the specified destination array of the same type T. diff --git a/src/Split/net47/Polyfill_ConcurrentDictionary.cs b/src/Split/net47/Polyfill_ConcurrentDictionary.cs index 8805de34b..a3547dd29 100644 --- a/src/Split/net47/Polyfill_ConcurrentDictionary.cs +++ b/src/Split/net47/Polyfill_ConcurrentDictionary.cs @@ -12,6 +12,10 @@ static partial class Polyfill public static TValue GetOrAdd(this ConcurrentDictionary target, TKey key, Func valueFactory, TArg factoryArgument) where TKey : notnull { + if (valueFactory == null) + { + throw new ArgumentNullException(nameof(valueFactory)); + } while (true) { TValue value; diff --git a/src/Split/net47/Polyfill_Encoding_GetChars.cs b/src/Split/net47/Polyfill_Encoding_GetChars.cs index f5a7de80b..4b956e845 100644 --- a/src/Split/net47/Polyfill_Encoding_GetChars.cs +++ b/src/Split/net47/Polyfill_Encoding_GetChars.cs @@ -25,7 +25,7 @@ public static int GetChars(this Encoding target, ReadOnlySpan bytes, Span< var charArray = new char[bytes.Length]; var array = bytes.ToArray(); var count = target.GetChars(array, 0, bytes.Length, charArray, 0); - new ReadOnlySpan(charArray).CopyTo(chars); + new ReadOnlySpan(charArray, 0, count).CopyTo(chars); return count; } #endif diff --git a/src/Split/net47/Polyfill_List.cs b/src/Split/net47/Polyfill_List.cs index c2e76ed77..7a9348d0c 100644 --- a/src/Split/net47/Polyfill_List.cs +++ b/src/Split/net47/Polyfill_List.cs @@ -21,6 +21,10 @@ public static void AddRange(this List target, ReadOnlySpan source) /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { + if ((uint)index > (uint)target.Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } for (var i = 0; i < source.Length; i++) { var item = source[i]; @@ -30,6 +34,10 @@ public static void InsertRange(this List target, int index, ReadOnlySpanCopies the entire to a span. public static void CopyTo(this List target, Span destination) { + if (target.Count > destination.Length) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } for (var index = 0; index < target.Count; index++) { destination[index] = target[index]; diff --git a/src/Split/net47/Polyfill_Memory_SpanSort.cs b/src/Split/net47/Polyfill_Memory_SpanSort.cs index acc5c8666..b30957972 100644 --- a/src/Split/net47/Polyfill_Memory_SpanSort.cs +++ b/src/Split/net47/Polyfill_Memory_SpanSort.cs @@ -77,12 +77,8 @@ internal ComparerWrapper(Comparison comparison) throw new ArgumentNullException(nameof(comparison)); this.comparison = comparison; } - public int Compare(T? x, T? y) - { - if (x is null || y is null) - return 0; - return comparison((T)x, (T)y); - } + public int Compare(T? x, T? y) => + comparison(x!, y!); } } #endif diff --git a/src/Split/net47/Polyfill_MicroNanosecondAdd.cs b/src/Split/net47/Polyfill_MicroNanosecondAdd.cs index 17d2c8d00..e099e1260 100644 --- a/src/Split/net47/Polyfill_MicroNanosecondAdd.cs +++ b/src/Split/net47/Polyfill_MicroNanosecondAdd.cs @@ -8,10 +8,10 @@ static partial class Polyfill /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTime AddMicroseconds(this DateTime target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); /// /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTimeOffset AddMicroseconds(this DateTimeOffset target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); } diff --git a/src/Split/net47/Polyfill_String.cs b/src/Split/net47/Polyfill_String.cs index bab55956e..254f4be0a 100644 --- a/src/Split/net47/Polyfill_String.cs +++ b/src/Split/net47/Polyfill_String.cs @@ -3,8 +3,6 @@ namespace Polyfills; using System; using System.Text; -using System.IO; -using System.Runtime.CompilerServices; static partial class Polyfill { #if FeatureMemory @@ -147,22 +145,49 @@ public static int LastIndexOf(this string target, char value, int startIndex, in /// /// Replaces all newline sequences in the current string with . /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ReplaceLineEndings(this string target, string replacementText) { + var index = IndexOfNewlineChar(target, 0, out var stride); + if (index < 0) + { + return target; + } var builder = new StringBuilder(target.Length); - using var reader = new StringReader(target); + var position = 0; while (true) { - var line = reader.ReadLine(); - if (line == null) + builder.Append(target, position, index - position); + builder.Append(replacementText); + position = index + stride; + index = IndexOfNewlineChar(target, position, out stride); + if (index < 0) { break; } - builder.Append(line); - builder.Append(replacementText); } - return builder.ToString(0, builder.Length - replacementText.Length); + builder.Append(target, position, target.Length - position); + return builder.ToString(); + } + static int IndexOfNewlineChar(string text, int startIndex, out int stride) + { + stride = 0; + for (var index = startIndex; index < text.Length; index++) + { + switch (text[index]) + { + case '\r': + stride = index + 1 < text.Length && text[index + 1] == '\n' ? 2 : 1; + return index; + case '\n': + case '\f': + case '\u0085': + case '\u2028': + case '\u2029': + stride = 1; + return index; + } + } + return -1; } /// /// Replaces all newline sequences in the current string with . diff --git a/src/Split/net47/Polyfill_StringBuilder_Append.cs b/src/Split/net47/Polyfill_StringBuilder_Append.cs index 0b60749a9..d3970ce53 100644 --- a/src/Split/net47/Polyfill_StringBuilder_Append.cs +++ b/src/Split/net47/Polyfill_StringBuilder_Append.cs @@ -10,6 +10,14 @@ static partial class Polyfill /// public static StringBuilder Append(this StringBuilder target, StringBuilder? value, int startIndex, int count) { + if (startIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(startIndex)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } if (value == null) { if (startIndex == 0 && count == 0) diff --git a/src/Split/net47/Polyfill_StringBuilder_CopyTo.cs b/src/Split/net47/Polyfill_StringBuilder_CopyTo.cs index dc625a7bc..ba99e0a1f 100644 --- a/src/Split/net47/Polyfill_StringBuilder_CopyTo.cs +++ b/src/Split/net47/Polyfill_StringBuilder_CopyTo.cs @@ -15,20 +15,25 @@ public static void CopyTo( Span destination, int count) { - var destinationIndex = 0; - while (true) + if (count < 0) { - if (sourceIndex == target.Length) - { - break; - } - if (destinationIndex == count) - { - break; - } - destination[destinationIndex] = target[sourceIndex]; - destinationIndex++; - sourceIndex++; + throw new ArgumentOutOfRangeException(nameof(count)); + } + if ((uint)sourceIndex > (uint)target.Length) + { + throw new ArgumentOutOfRangeException(nameof(sourceIndex)); + } + if (sourceIndex > target.Length - count) + { + throw new ArgumentException("The source StringBuilder does not contain enough characters from sourceIndex onward to satisfy count."); + } + if (count > destination.Length) + { + throw new ArgumentException("The destination span is too short to hold the requested characters.", nameof(destination)); + } + for (var index = 0; index < count; index++) + { + destination[index] = target[sourceIndex + index]; } } } diff --git a/src/Split/net47/Polyfill_Type.cs b/src/Split/net47/Polyfill_Type.cs index 7890e2168..b99bdfab5 100644 --- a/src/Split/net47/Polyfill_Type.cs +++ b/src/Split/net47/Polyfill_Type.cs @@ -7,9 +7,15 @@ namespace Polyfills; using System.Linq; static partial class Polyfill { - public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInfo other) => - target.MetadataToken == other.MetadataToken && - target.Module.Equals(other.Module); + public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInfo other) + { + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } + return target.MetadataToken == other.MetadataToken && + target.Module.Equals(other.Module); + } /// /// Searches for the specified method whose parameters match the specified generic parameter count, argument types and modifiers, using the specified binding constraints. /// @@ -22,6 +28,25 @@ public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInf Type[] types, ParameterModifier[]? modifiers) { + if (name is null) + { + throw new ArgumentNullException(nameof(name)); + } + if (genericParameterCount < 0) + { + throw new ArgumentOutOfRangeException(nameof(genericParameterCount)); + } + if (types is null) + { + throw new ArgumentNullException(nameof(types)); + } + foreach (var type in types) + { + if (type is null) + { + throw new ArgumentNullException(nameof(types)); + } + } var methods = target.GetMethods(bindingAttr); if (genericParameterCount == 0) { diff --git a/src/Split/net47/RandomNumberGeneratorPolyfill.cs b/src/Split/net47/RandomNumberGeneratorPolyfill.cs index 08384a7ea..539aa7aba 100644 --- a/src/Split/net47/RandomNumberGeneratorPolyfill.cs +++ b/src/Split/net47/RandomNumberGeneratorPolyfill.cs @@ -34,7 +34,7 @@ public static int GetInt32(int fromInclusive, int toExclusive) { generator.GetBytes(bytes); value = BitConverter.ToUInt32(bytes, 0) & mask; - } while (value >= range); + } while (value > range); return (int) (fromInclusive + value); } /// diff --git a/src/Split/net47/StringPolyfill.cs b/src/Split/net47/StringPolyfill.cs index bbdee2733..b14512f1d 100644 --- a/src/Split/net47/StringPolyfill.cs +++ b/src/Split/net47/StringPolyfill.cs @@ -50,6 +50,10 @@ public static string Join(char separator, scoped ReadOnlySpan values) span = span.Slice(1); } var value = values[index]; + if (value is null) + { + continue; + } value.CopyTo(span); span = span.Slice(value.Length); } diff --git a/src/Split/net47/TaskCompletionSource.cs b/src/Split/net47/TaskCompletionSource.cs index 04b507f76..67a35f938 100644 --- a/src/Split/net47/TaskCompletionSource.cs +++ b/src/Split/net47/TaskCompletionSource.cs @@ -77,5 +77,5 @@ public TaskCompletionSource(object? state, TaskCreationOptions creationOptions) /// /// Attempts to transition the underlying into the state. /// - public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(default); + public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(cancellationToken); } diff --git a/src/Split/net47/TimeSpanPolyfill.cs b/src/Split/net47/TimeSpanPolyfill.cs index 94d13cbbb..59d7c7974 100644 --- a/src/Split/net47/TimeSpanPolyfill.cs +++ b/src/Split/net47/TimeSpanPolyfill.cs @@ -15,7 +15,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microseconds => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new TimeSpan(target.Days, target.Hours, target.Minutes, 0); @@ -24,52 +24,71 @@ long TicksComponent() } } const long MicrosecondsToTicks = TimeSpan.TicksPerMillisecond / 1000; + const long MicrosecondsPerMillisecond = 1_000; + const long MicrosecondsPerSecond = 1_000_000; + const long MicrosecondsPerMinute = 60_000_000; + const long MicrosecondsPerHour = 3_600_000_000; + const long MicrosecondsPerDay = 86_400_000_000; + static TimeSpan TicksFromMicroseconds(decimal totalMicroseconds) + { + var ticks = totalMicroseconds * MicrosecondsToTicks; + if (ticks < long.MinValue || ticks > long.MaxValue) + { + throw new ArgumentOutOfRangeException(null, "TimeSpan overflowed because the duration is too long."); + } + return new((long)ticks); + } extension(TimeSpan) { /// /// Initializes a new instance of the structure to a specified number of days, hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromDays(int days, int hours = 0, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)days * TimeSpan.TicksPerDay + - (long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)days * MicrosecondsPerDay + + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromHours(int hours, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromMinutes(int minutes, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of seconds, milliseconds, and microseconds. /// public static TimeSpan FromSeconds(long seconds, long milliseconds = 0, long microseconds = 0) => - new(seconds * TimeSpan.TicksPerSecond + - milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of milliseconds and microseconds. /// public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0) => - new(milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of microseconds. /// public static TimeSpan FromMicroseconds(long microseconds) => - new(microseconds * MicrosecondsToTicks); + TicksFromMicroseconds(microseconds); } } diff --git a/src/Split/net47/XDocumentPolyfill.cs b/src/Split/net47/XDocumentPolyfill.cs index 1611acf19..876842223 100644 --- a/src/Split/net47/XDocumentPolyfill.cs +++ b/src/Split/net47/XDocumentPolyfill.cs @@ -25,12 +25,10 @@ static partial class XDocumentPolyfill /// /// Asynchronously creates a new XDocument and initializes its underlying XML tree using the specified stream, optionally preserving white space. /// - public static async Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) + public static Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - using var reader = new StreamReader(stream); - var content = await reader.ReadToEndAsync(); - return XDocument.Parse(content, options); + return Task.FromResult(XDocument.Load(stream, options)); } /// /// Asynchronously creates a new XDocument and initializes its underlying XML tree using the specified text reader, optionally preserving white space. diff --git a/src/Split/net47/XElementPolyfill.cs b/src/Split/net47/XElementPolyfill.cs index 27208879a..9347d9cdd 100644 --- a/src/Split/net47/XElementPolyfill.cs +++ b/src/Split/net47/XElementPolyfill.cs @@ -25,12 +25,10 @@ static partial class XElementPolyfill /// /// Asynchronously creates a new XElement and initializes its underlying XML tree using the specified stream, optionally preserving white space. /// - public static async Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) + public static Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - using var reader = new StreamReader(stream); - var content = await reader.ReadToEndAsync(); - return XElement.Parse(content, options); + return Task.FromResult(XElement.Load(stream, options)); } /// /// Asynchronously creates a new XElement and initializes its underlying XML tree using the specified text reader, optionally preserving white space. diff --git a/src/Split/net47/ZipFilePolyfill.cs b/src/Split/net47/ZipFilePolyfill.cs index 6cb361e6f..d908c5633 100644 --- a/src/Split/net47/ZipFilePolyfill.cs +++ b/src/Split/net47/ZipFilePolyfill.cs @@ -53,11 +53,12 @@ static void ExtractToDirectoryPolyfill( string destinationDirectory, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + return; } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, null); } static void ExtractToDirectoryPolyfill( string sourceArchiveFile, @@ -65,11 +66,43 @@ static void ExtractToDirectoryPolyfill( Encoding encoding, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); + return; + } + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, encoding); + } + static void ExtractToDirectoryWithOverwrite( + string sourceArchiveFile, + string destinationDirectory, + Encoding? encoding) + { + var destinationRoot = Path.GetFullPath(destinationDirectory); + Directory.CreateDirectory(destinationRoot); + var normalizedRoot = destinationRoot; + if (normalizedRoot[normalizedRoot.Length - 1] != Path.DirectorySeparatorChar) + { + normalizedRoot += Path.DirectorySeparatorChar; + } + using var archive = encoding == null + ? ZipFile.OpenRead(sourceArchiveFile) + : ZipFile.Open(sourceArchiveFile, ZipArchiveMode.Read, encoding); + foreach (var entry in archive.Entries) + { + var destinationPath = Path.GetFullPath(Path.Combine(destinationRoot, entry.FullName)); + if (!destinationPath.StartsWith(normalizedRoot, StringComparison.Ordinal)) + { + throw new IOException("Extracting Zip entry would have resulted in a file outside the specified destination directory."); + } + if (entry.Name.Length == 0) + { + Directory.CreateDirectory(destinationPath); + continue; + } + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + entry.ExtractToFile(destinationPath, overwrite: true); } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); } /// /// Asynchronously opens a ZipArchive on the specified archiveFileName in the specified ZipArchiveMode mode. diff --git a/src/Split/net471/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs b/src/Split/net471/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs index 32b0afded..8bad1d83d 100644 --- a/src/Split/net471/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs +++ b/src/Split/net471/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs @@ -15,7 +15,7 @@ public static void ThrowIfZero(T value, [CallerArgumentExpression(nameof(valu { if (value.Equals(default)) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } /// Throws an if is zero. @@ -23,12 +23,12 @@ public static void ThrowIfZero(nint value, [CallerArgumentExpression(nameof(valu { if (value == 0) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } [DoesNotReturn] - static void ThrowZero(string? paramName) => - throw new ArgumentOutOfRangeException(paramName, "Value must not be zero."); + static void ThrowZero(T value, string? paramName) => + throw new ArgumentOutOfRangeException(paramName, value, $"{paramName} ('{value}') must be a non-zero value."); public static void ThrowIfNegative(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : struct, IComparable { @@ -112,7 +112,7 @@ public static void ThrowIfGreaterThanOrEqual(nint value, nint other, [CallerArgu { if (value >= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than {other}."); } } public static void ThrowIfLessThan(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) @@ -144,7 +144,7 @@ public static void ThrowIfLessThanOrEqual(nint value, nint other, [CallerArgumen { if (value <= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than {other}."); } } } diff --git a/src/Split/net471/ConvertPolyfill.cs b/src/Split/net471/ConvertPolyfill.cs index 412674710..1f45391f9 100644 --- a/src/Split/net471/ConvertPolyfill.cs +++ b/src/Split/net471/ConvertPolyfill.cs @@ -134,19 +134,19 @@ public static OperationStatus FromHexString(ReadOnlySpan source, Span= destination.Length) { charsConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal(source[i]); + var lo = GetHexVal(source[i + 1]); + if (hi < 0 || lo < 0) { - charsConsumed = i; + charsConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; @@ -193,19 +193,19 @@ public static OperationStatus FromHexString(ReadOnlySpan utf8Source, Span< var j = 0; while (i + 1 < utf8Source.Length) { - var hi = GetHexVal((char)utf8Source[i]); - var lo = GetHexVal((char)utf8Source[i + 1]); - if (hi < 0 || lo < 0) + if (j >= destination.Length) { bytesConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal((char)utf8Source[i]); + var lo = GetHexVal((char)utf8Source[i + 1]); + if (hi < 0 || lo < 0) { - bytesConsumed = i; + bytesConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; diff --git a/src/Split/net471/DateTimeOffsetPolyfill.cs b/src/Split/net471/DateTimeOffsetPolyfill.cs index 6837790e5..012b944dc 100644 --- a/src/Split/net471/DateTimeOffsetPolyfill.cs +++ b/src/Split/net471/DateTimeOffsetPolyfill.cs @@ -17,7 +17,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTimeOffset(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Offset); diff --git a/src/Split/net471/DateTimePolyfill.cs b/src/Split/net471/DateTimePolyfill.cs index 4641f3e59..430683e14 100644 --- a/src/Split/net471/DateTimePolyfill.cs +++ b/src/Split/net471/DateTimePolyfill.cs @@ -16,7 +16,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTime(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Kind); diff --git a/src/Split/net471/EqualityComparerPolyfill.cs b/src/Split/net471/EqualityComparerPolyfill.cs index e45f2a30e..55cb3ad44 100644 --- a/src/Split/net471/EqualityComparerPolyfill.cs +++ b/src/Split/net471/EqualityComparerPolyfill.cs @@ -19,8 +19,14 @@ public override int GetHashCode(T obj) => /// public static EqualityComparer Create( Func equals, - Func? getHashCode = null) => - new DelegateEqualityComparer(equals, getHashCode); + Func? getHashCode = null) + { + if (equals is null) + { + throw new ArgumentNullException(nameof(equals)); + } + return new DelegateEqualityComparer(equals, getHashCode); + } } class KeySelectorEqualityComparer(Func keySelector, IEqualityComparer? keyComparer) : EqualityComparer @@ -41,7 +47,13 @@ public override int GetHashCode(T obj) /// public static EqualityComparer Create( Func keySelector, - IEqualityComparer? keyComparer = null) => - new KeySelectorEqualityComparer(keySelector, keyComparer); + IEqualityComparer? keyComparer = null) + { + if (keySelector is null) + { + throw new ArgumentNullException(nameof(keySelector)); + } + return new KeySelectorEqualityComparer(keySelector, keyComparer); + } } } diff --git a/src/Split/net471/FileUnixModePolyfill.cs b/src/Split/net471/FileUnixModePolyfill.cs index 2440f7800..701ef07cb 100644 --- a/src/Split/net471/FileUnixModePolyfill.cs +++ b/src/Split/net471/FileUnixModePolyfill.cs @@ -47,8 +47,8 @@ public static UnixFileMode GetUnixFileMode(string path) var mode = output[0] switch { '1' => UnixFileMode.StickyBit, - '2' => UnixFileMode.SetUser, - '4' => UnixFileMode.SetGroup, + '2' => UnixFileMode.SetGroup, + '4' => UnixFileMode.SetUser, _ => UnixFileMode.None }; mode = output[1] switch @@ -158,13 +158,13 @@ public static void SetUnixFileMode(string path, UnixFileMode mode) octal[3] += 1; break; case "setuser": - octal[0] += 2; + octal[0] += 4; break; case "stickybit": octal[0] += 1; break; case "setgroup": - octal[0] += 4; + octal[0] += 2; break; default: throw new("Invalid notation detected"); diff --git a/src/Split/net471/Numbers/DoublePolyfill.cs b/src/Split/net471/Numbers/DoublePolyfill.cs index ba5333f69..2e3e40efd 100644 --- a/src/Split/net471/Numbers/DoublePolyfill.cs +++ b/src/Split/net471/Numbers/DoublePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out double result) => - double.TryParse(s, NumberStyles.Float, provider, out result); + double.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,7 +28,7 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its double-precision floating-point number equivalent.. /// public static bool TryParse(ReadOnlySpan utf8Text, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Converts the span representation of a number in a specified style and culture-specific format to its double-precision floating-point number equivalent. A return value indicates whether the conversion succeeded or failed. /// @@ -43,7 +43,7 @@ public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatPro /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out double result) => - double.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + double.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/net471/Numbers/SinglePolyfill.cs b/src/Split/net471/Numbers/SinglePolyfill.cs index 69af78095..2f6e02a82 100644 --- a/src/Split/net471/Numbers/SinglePolyfill.cs +++ b/src/Split/net471/Numbers/SinglePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out float result) => - float.TryParse(s, NumberStyles.Float, provider, out result); + float.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,7 +28,7 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its single-precision floating-point number equivalent. /// public static bool TryParse(ReadOnlySpan utf8Text, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Converts the span representation of a number in a specified style and culture-specific format to its single-precision floating-point number equivalent. A return value indicates whether the conversion succeeded or failed. /// @@ -43,7 +43,7 @@ public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatPro /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out float result) => - float.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + float.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/net471/Polyfill.cs b/src/Split/net471/Polyfill.cs index 16e916262..b7b21af17 100644 --- a/src/Split/net471/Polyfill.cs +++ b/src/Split/net471/Polyfill.cs @@ -17,5 +17,5 @@ namespace Polyfills; #endif static partial class Polyfill { - const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond * 1000; + const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; } diff --git a/src/Split/net471/Polyfill_ArraySegment.cs b/src/Split/net471/Polyfill_ArraySegment.cs index 96e83c18d..05ea1f659 100644 --- a/src/Split/net471/Polyfill_ArraySegment.cs +++ b/src/Split/net471/Polyfill_ArraySegment.cs @@ -12,15 +12,19 @@ static partial class Polyfill /// public static void CopyTo(this ArraySegment target, ArraySegment destination) { - if (target.Count > destination.Count) + if (target.Array == null) { - throw new ArgumentException("DestinationTooShort", nameof(destination)); + throw new InvalidOperationException("InvalidOperation_NullArray"); } - if (target.Array == null) + if (destination.Array == null) { throw new InvalidOperationException("InvalidOperation_NullArray"); } - Array.Copy(target.Array!, target.Offset, destination.Array, destination.Offset, target.Count); + if (target.Count > destination.Count) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } + Array.Copy(target.Array, target.Offset, destination.Array, destination.Offset, target.Count); } /// /// Copies the contents of this instance into the specified destination array of the same type T. diff --git a/src/Split/net471/Polyfill_ConcurrentDictionary.cs b/src/Split/net471/Polyfill_ConcurrentDictionary.cs index 8805de34b..a3547dd29 100644 --- a/src/Split/net471/Polyfill_ConcurrentDictionary.cs +++ b/src/Split/net471/Polyfill_ConcurrentDictionary.cs @@ -12,6 +12,10 @@ static partial class Polyfill public static TValue GetOrAdd(this ConcurrentDictionary target, TKey key, Func valueFactory, TArg factoryArgument) where TKey : notnull { + if (valueFactory == null) + { + throw new ArgumentNullException(nameof(valueFactory)); + } while (true) { TValue value; diff --git a/src/Split/net471/Polyfill_Encoding_GetChars.cs b/src/Split/net471/Polyfill_Encoding_GetChars.cs index f5a7de80b..4b956e845 100644 --- a/src/Split/net471/Polyfill_Encoding_GetChars.cs +++ b/src/Split/net471/Polyfill_Encoding_GetChars.cs @@ -25,7 +25,7 @@ public static int GetChars(this Encoding target, ReadOnlySpan bytes, Span< var charArray = new char[bytes.Length]; var array = bytes.ToArray(); var count = target.GetChars(array, 0, bytes.Length, charArray, 0); - new ReadOnlySpan(charArray).CopyTo(chars); + new ReadOnlySpan(charArray, 0, count).CopyTo(chars); return count; } #endif diff --git a/src/Split/net471/Polyfill_List.cs b/src/Split/net471/Polyfill_List.cs index c2e76ed77..7a9348d0c 100644 --- a/src/Split/net471/Polyfill_List.cs +++ b/src/Split/net471/Polyfill_List.cs @@ -21,6 +21,10 @@ public static void AddRange(this List target, ReadOnlySpan source) /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { + if ((uint)index > (uint)target.Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } for (var i = 0; i < source.Length; i++) { var item = source[i]; @@ -30,6 +34,10 @@ public static void InsertRange(this List target, int index, ReadOnlySpanCopies the entire to a span. public static void CopyTo(this List target, Span destination) { + if (target.Count > destination.Length) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } for (var index = 0; index < target.Count; index++) { destination[index] = target[index]; diff --git a/src/Split/net471/Polyfill_Memory_SpanSort.cs b/src/Split/net471/Polyfill_Memory_SpanSort.cs index acc5c8666..b30957972 100644 --- a/src/Split/net471/Polyfill_Memory_SpanSort.cs +++ b/src/Split/net471/Polyfill_Memory_SpanSort.cs @@ -77,12 +77,8 @@ internal ComparerWrapper(Comparison comparison) throw new ArgumentNullException(nameof(comparison)); this.comparison = comparison; } - public int Compare(T? x, T? y) - { - if (x is null || y is null) - return 0; - return comparison((T)x, (T)y); - } + public int Compare(T? x, T? y) => + comparison(x!, y!); } } #endif diff --git a/src/Split/net471/Polyfill_MicroNanosecondAdd.cs b/src/Split/net471/Polyfill_MicroNanosecondAdd.cs index 17d2c8d00..e099e1260 100644 --- a/src/Split/net471/Polyfill_MicroNanosecondAdd.cs +++ b/src/Split/net471/Polyfill_MicroNanosecondAdd.cs @@ -8,10 +8,10 @@ static partial class Polyfill /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTime AddMicroseconds(this DateTime target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); /// /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTimeOffset AddMicroseconds(this DateTimeOffset target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); } diff --git a/src/Split/net471/Polyfill_String.cs b/src/Split/net471/Polyfill_String.cs index bab55956e..254f4be0a 100644 --- a/src/Split/net471/Polyfill_String.cs +++ b/src/Split/net471/Polyfill_String.cs @@ -3,8 +3,6 @@ namespace Polyfills; using System; using System.Text; -using System.IO; -using System.Runtime.CompilerServices; static partial class Polyfill { #if FeatureMemory @@ -147,22 +145,49 @@ public static int LastIndexOf(this string target, char value, int startIndex, in /// /// Replaces all newline sequences in the current string with . /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ReplaceLineEndings(this string target, string replacementText) { + var index = IndexOfNewlineChar(target, 0, out var stride); + if (index < 0) + { + return target; + } var builder = new StringBuilder(target.Length); - using var reader = new StringReader(target); + var position = 0; while (true) { - var line = reader.ReadLine(); - if (line == null) + builder.Append(target, position, index - position); + builder.Append(replacementText); + position = index + stride; + index = IndexOfNewlineChar(target, position, out stride); + if (index < 0) { break; } - builder.Append(line); - builder.Append(replacementText); } - return builder.ToString(0, builder.Length - replacementText.Length); + builder.Append(target, position, target.Length - position); + return builder.ToString(); + } + static int IndexOfNewlineChar(string text, int startIndex, out int stride) + { + stride = 0; + for (var index = startIndex; index < text.Length; index++) + { + switch (text[index]) + { + case '\r': + stride = index + 1 < text.Length && text[index + 1] == '\n' ? 2 : 1; + return index; + case '\n': + case '\f': + case '\u0085': + case '\u2028': + case '\u2029': + stride = 1; + return index; + } + } + return -1; } /// /// Replaces all newline sequences in the current string with . diff --git a/src/Split/net471/Polyfill_StringBuilder_Append.cs b/src/Split/net471/Polyfill_StringBuilder_Append.cs index 0b60749a9..d3970ce53 100644 --- a/src/Split/net471/Polyfill_StringBuilder_Append.cs +++ b/src/Split/net471/Polyfill_StringBuilder_Append.cs @@ -10,6 +10,14 @@ static partial class Polyfill /// public static StringBuilder Append(this StringBuilder target, StringBuilder? value, int startIndex, int count) { + if (startIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(startIndex)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } if (value == null) { if (startIndex == 0 && count == 0) diff --git a/src/Split/net471/Polyfill_StringBuilder_CopyTo.cs b/src/Split/net471/Polyfill_StringBuilder_CopyTo.cs index dc625a7bc..ba99e0a1f 100644 --- a/src/Split/net471/Polyfill_StringBuilder_CopyTo.cs +++ b/src/Split/net471/Polyfill_StringBuilder_CopyTo.cs @@ -15,20 +15,25 @@ public static void CopyTo( Span destination, int count) { - var destinationIndex = 0; - while (true) + if (count < 0) { - if (sourceIndex == target.Length) - { - break; - } - if (destinationIndex == count) - { - break; - } - destination[destinationIndex] = target[sourceIndex]; - destinationIndex++; - sourceIndex++; + throw new ArgumentOutOfRangeException(nameof(count)); + } + if ((uint)sourceIndex > (uint)target.Length) + { + throw new ArgumentOutOfRangeException(nameof(sourceIndex)); + } + if (sourceIndex > target.Length - count) + { + throw new ArgumentException("The source StringBuilder does not contain enough characters from sourceIndex onward to satisfy count."); + } + if (count > destination.Length) + { + throw new ArgumentException("The destination span is too short to hold the requested characters.", nameof(destination)); + } + for (var index = 0; index < count; index++) + { + destination[index] = target[sourceIndex + index]; } } } diff --git a/src/Split/net471/Polyfill_Type.cs b/src/Split/net471/Polyfill_Type.cs index 7890e2168..b99bdfab5 100644 --- a/src/Split/net471/Polyfill_Type.cs +++ b/src/Split/net471/Polyfill_Type.cs @@ -7,9 +7,15 @@ namespace Polyfills; using System.Linq; static partial class Polyfill { - public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInfo other) => - target.MetadataToken == other.MetadataToken && - target.Module.Equals(other.Module); + public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInfo other) + { + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } + return target.MetadataToken == other.MetadataToken && + target.Module.Equals(other.Module); + } /// /// Searches for the specified method whose parameters match the specified generic parameter count, argument types and modifiers, using the specified binding constraints. /// @@ -22,6 +28,25 @@ public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInf Type[] types, ParameterModifier[]? modifiers) { + if (name is null) + { + throw new ArgumentNullException(nameof(name)); + } + if (genericParameterCount < 0) + { + throw new ArgumentOutOfRangeException(nameof(genericParameterCount)); + } + if (types is null) + { + throw new ArgumentNullException(nameof(types)); + } + foreach (var type in types) + { + if (type is null) + { + throw new ArgumentNullException(nameof(types)); + } + } var methods = target.GetMethods(bindingAttr); if (genericParameterCount == 0) { diff --git a/src/Split/net471/RandomNumberGeneratorPolyfill.cs b/src/Split/net471/RandomNumberGeneratorPolyfill.cs index 08384a7ea..539aa7aba 100644 --- a/src/Split/net471/RandomNumberGeneratorPolyfill.cs +++ b/src/Split/net471/RandomNumberGeneratorPolyfill.cs @@ -34,7 +34,7 @@ public static int GetInt32(int fromInclusive, int toExclusive) { generator.GetBytes(bytes); value = BitConverter.ToUInt32(bytes, 0) & mask; - } while (value >= range); + } while (value > range); return (int) (fromInclusive + value); } /// diff --git a/src/Split/net471/StringPolyfill.cs b/src/Split/net471/StringPolyfill.cs index bbdee2733..b14512f1d 100644 --- a/src/Split/net471/StringPolyfill.cs +++ b/src/Split/net471/StringPolyfill.cs @@ -50,6 +50,10 @@ public static string Join(char separator, scoped ReadOnlySpan values) span = span.Slice(1); } var value = values[index]; + if (value is null) + { + continue; + } value.CopyTo(span); span = span.Slice(value.Length); } diff --git a/src/Split/net471/TaskCompletionSource.cs b/src/Split/net471/TaskCompletionSource.cs index 04b507f76..67a35f938 100644 --- a/src/Split/net471/TaskCompletionSource.cs +++ b/src/Split/net471/TaskCompletionSource.cs @@ -77,5 +77,5 @@ public TaskCompletionSource(object? state, TaskCreationOptions creationOptions) /// /// Attempts to transition the underlying into the state. /// - public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(default); + public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(cancellationToken); } diff --git a/src/Split/net471/TimeSpanPolyfill.cs b/src/Split/net471/TimeSpanPolyfill.cs index 94d13cbbb..59d7c7974 100644 --- a/src/Split/net471/TimeSpanPolyfill.cs +++ b/src/Split/net471/TimeSpanPolyfill.cs @@ -15,7 +15,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microseconds => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new TimeSpan(target.Days, target.Hours, target.Minutes, 0); @@ -24,52 +24,71 @@ long TicksComponent() } } const long MicrosecondsToTicks = TimeSpan.TicksPerMillisecond / 1000; + const long MicrosecondsPerMillisecond = 1_000; + const long MicrosecondsPerSecond = 1_000_000; + const long MicrosecondsPerMinute = 60_000_000; + const long MicrosecondsPerHour = 3_600_000_000; + const long MicrosecondsPerDay = 86_400_000_000; + static TimeSpan TicksFromMicroseconds(decimal totalMicroseconds) + { + var ticks = totalMicroseconds * MicrosecondsToTicks; + if (ticks < long.MinValue || ticks > long.MaxValue) + { + throw new ArgumentOutOfRangeException(null, "TimeSpan overflowed because the duration is too long."); + } + return new((long)ticks); + } extension(TimeSpan) { /// /// Initializes a new instance of the structure to a specified number of days, hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromDays(int days, int hours = 0, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)days * TimeSpan.TicksPerDay + - (long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)days * MicrosecondsPerDay + + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromHours(int hours, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromMinutes(int minutes, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of seconds, milliseconds, and microseconds. /// public static TimeSpan FromSeconds(long seconds, long milliseconds = 0, long microseconds = 0) => - new(seconds * TimeSpan.TicksPerSecond + - milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of milliseconds and microseconds. /// public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0) => - new(milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of microseconds. /// public static TimeSpan FromMicroseconds(long microseconds) => - new(microseconds * MicrosecondsToTicks); + TicksFromMicroseconds(microseconds); } } diff --git a/src/Split/net471/XDocumentPolyfill.cs b/src/Split/net471/XDocumentPolyfill.cs index 1611acf19..876842223 100644 --- a/src/Split/net471/XDocumentPolyfill.cs +++ b/src/Split/net471/XDocumentPolyfill.cs @@ -25,12 +25,10 @@ static partial class XDocumentPolyfill /// /// Asynchronously creates a new XDocument and initializes its underlying XML tree using the specified stream, optionally preserving white space. /// - public static async Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) + public static Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - using var reader = new StreamReader(stream); - var content = await reader.ReadToEndAsync(); - return XDocument.Parse(content, options); + return Task.FromResult(XDocument.Load(stream, options)); } /// /// Asynchronously creates a new XDocument and initializes its underlying XML tree using the specified text reader, optionally preserving white space. diff --git a/src/Split/net471/XElementPolyfill.cs b/src/Split/net471/XElementPolyfill.cs index 27208879a..9347d9cdd 100644 --- a/src/Split/net471/XElementPolyfill.cs +++ b/src/Split/net471/XElementPolyfill.cs @@ -25,12 +25,10 @@ static partial class XElementPolyfill /// /// Asynchronously creates a new XElement and initializes its underlying XML tree using the specified stream, optionally preserving white space. /// - public static async Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) + public static Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - using var reader = new StreamReader(stream); - var content = await reader.ReadToEndAsync(); - return XElement.Parse(content, options); + return Task.FromResult(XElement.Load(stream, options)); } /// /// Asynchronously creates a new XElement and initializes its underlying XML tree using the specified text reader, optionally preserving white space. diff --git a/src/Split/net471/ZipFilePolyfill.cs b/src/Split/net471/ZipFilePolyfill.cs index 6cb361e6f..d908c5633 100644 --- a/src/Split/net471/ZipFilePolyfill.cs +++ b/src/Split/net471/ZipFilePolyfill.cs @@ -53,11 +53,12 @@ static void ExtractToDirectoryPolyfill( string destinationDirectory, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + return; } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, null); } static void ExtractToDirectoryPolyfill( string sourceArchiveFile, @@ -65,11 +66,43 @@ static void ExtractToDirectoryPolyfill( Encoding encoding, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); + return; + } + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, encoding); + } + static void ExtractToDirectoryWithOverwrite( + string sourceArchiveFile, + string destinationDirectory, + Encoding? encoding) + { + var destinationRoot = Path.GetFullPath(destinationDirectory); + Directory.CreateDirectory(destinationRoot); + var normalizedRoot = destinationRoot; + if (normalizedRoot[normalizedRoot.Length - 1] != Path.DirectorySeparatorChar) + { + normalizedRoot += Path.DirectorySeparatorChar; + } + using var archive = encoding == null + ? ZipFile.OpenRead(sourceArchiveFile) + : ZipFile.Open(sourceArchiveFile, ZipArchiveMode.Read, encoding); + foreach (var entry in archive.Entries) + { + var destinationPath = Path.GetFullPath(Path.Combine(destinationRoot, entry.FullName)); + if (!destinationPath.StartsWith(normalizedRoot, StringComparison.Ordinal)) + { + throw new IOException("Extracting Zip entry would have resulted in a file outside the specified destination directory."); + } + if (entry.Name.Length == 0) + { + Directory.CreateDirectory(destinationPath); + continue; + } + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + entry.ExtractToFile(destinationPath, overwrite: true); } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); } /// /// Asynchronously opens a ZipArchive on the specified archiveFileName in the specified ZipArchiveMode mode. diff --git a/src/Split/net472/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs b/src/Split/net472/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs index 32b0afded..8bad1d83d 100644 --- a/src/Split/net472/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs +++ b/src/Split/net472/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs @@ -15,7 +15,7 @@ public static void ThrowIfZero(T value, [CallerArgumentExpression(nameof(valu { if (value.Equals(default)) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } /// Throws an if is zero. @@ -23,12 +23,12 @@ public static void ThrowIfZero(nint value, [CallerArgumentExpression(nameof(valu { if (value == 0) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } [DoesNotReturn] - static void ThrowZero(string? paramName) => - throw new ArgumentOutOfRangeException(paramName, "Value must not be zero."); + static void ThrowZero(T value, string? paramName) => + throw new ArgumentOutOfRangeException(paramName, value, $"{paramName} ('{value}') must be a non-zero value."); public static void ThrowIfNegative(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : struct, IComparable { @@ -112,7 +112,7 @@ public static void ThrowIfGreaterThanOrEqual(nint value, nint other, [CallerArgu { if (value >= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than {other}."); } } public static void ThrowIfLessThan(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) @@ -144,7 +144,7 @@ public static void ThrowIfLessThanOrEqual(nint value, nint other, [CallerArgumen { if (value <= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than {other}."); } } } diff --git a/src/Split/net472/ConvertPolyfill.cs b/src/Split/net472/ConvertPolyfill.cs index 412674710..1f45391f9 100644 --- a/src/Split/net472/ConvertPolyfill.cs +++ b/src/Split/net472/ConvertPolyfill.cs @@ -134,19 +134,19 @@ public static OperationStatus FromHexString(ReadOnlySpan source, Span= destination.Length) { charsConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal(source[i]); + var lo = GetHexVal(source[i + 1]); + if (hi < 0 || lo < 0) { - charsConsumed = i; + charsConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; @@ -193,19 +193,19 @@ public static OperationStatus FromHexString(ReadOnlySpan utf8Source, Span< var j = 0; while (i + 1 < utf8Source.Length) { - var hi = GetHexVal((char)utf8Source[i]); - var lo = GetHexVal((char)utf8Source[i + 1]); - if (hi < 0 || lo < 0) + if (j >= destination.Length) { bytesConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal((char)utf8Source[i]); + var lo = GetHexVal((char)utf8Source[i + 1]); + if (hi < 0 || lo < 0) { - bytesConsumed = i; + bytesConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; diff --git a/src/Split/net472/DateTimeOffsetPolyfill.cs b/src/Split/net472/DateTimeOffsetPolyfill.cs index 6837790e5..012b944dc 100644 --- a/src/Split/net472/DateTimeOffsetPolyfill.cs +++ b/src/Split/net472/DateTimeOffsetPolyfill.cs @@ -17,7 +17,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTimeOffset(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Offset); diff --git a/src/Split/net472/DateTimePolyfill.cs b/src/Split/net472/DateTimePolyfill.cs index 4641f3e59..430683e14 100644 --- a/src/Split/net472/DateTimePolyfill.cs +++ b/src/Split/net472/DateTimePolyfill.cs @@ -16,7 +16,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTime(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Kind); diff --git a/src/Split/net472/EqualityComparerPolyfill.cs b/src/Split/net472/EqualityComparerPolyfill.cs index e45f2a30e..55cb3ad44 100644 --- a/src/Split/net472/EqualityComparerPolyfill.cs +++ b/src/Split/net472/EqualityComparerPolyfill.cs @@ -19,8 +19,14 @@ public override int GetHashCode(T obj) => /// public static EqualityComparer Create( Func equals, - Func? getHashCode = null) => - new DelegateEqualityComparer(equals, getHashCode); + Func? getHashCode = null) + { + if (equals is null) + { + throw new ArgumentNullException(nameof(equals)); + } + return new DelegateEqualityComparer(equals, getHashCode); + } } class KeySelectorEqualityComparer(Func keySelector, IEqualityComparer? keyComparer) : EqualityComparer @@ -41,7 +47,13 @@ public override int GetHashCode(T obj) /// public static EqualityComparer Create( Func keySelector, - IEqualityComparer? keyComparer = null) => - new KeySelectorEqualityComparer(keySelector, keyComparer); + IEqualityComparer? keyComparer = null) + { + if (keySelector is null) + { + throw new ArgumentNullException(nameof(keySelector)); + } + return new KeySelectorEqualityComparer(keySelector, keyComparer); + } } } diff --git a/src/Split/net472/FileUnixModePolyfill.cs b/src/Split/net472/FileUnixModePolyfill.cs index 2440f7800..701ef07cb 100644 --- a/src/Split/net472/FileUnixModePolyfill.cs +++ b/src/Split/net472/FileUnixModePolyfill.cs @@ -47,8 +47,8 @@ public static UnixFileMode GetUnixFileMode(string path) var mode = output[0] switch { '1' => UnixFileMode.StickyBit, - '2' => UnixFileMode.SetUser, - '4' => UnixFileMode.SetGroup, + '2' => UnixFileMode.SetGroup, + '4' => UnixFileMode.SetUser, _ => UnixFileMode.None }; mode = output[1] switch @@ -158,13 +158,13 @@ public static void SetUnixFileMode(string path, UnixFileMode mode) octal[3] += 1; break; case "setuser": - octal[0] += 2; + octal[0] += 4; break; case "stickybit": octal[0] += 1; break; case "setgroup": - octal[0] += 4; + octal[0] += 2; break; default: throw new("Invalid notation detected"); diff --git a/src/Split/net472/Numbers/DoublePolyfill.cs b/src/Split/net472/Numbers/DoublePolyfill.cs index ba5333f69..2e3e40efd 100644 --- a/src/Split/net472/Numbers/DoublePolyfill.cs +++ b/src/Split/net472/Numbers/DoublePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out double result) => - double.TryParse(s, NumberStyles.Float, provider, out result); + double.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,7 +28,7 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its double-precision floating-point number equivalent.. /// public static bool TryParse(ReadOnlySpan utf8Text, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Converts the span representation of a number in a specified style and culture-specific format to its double-precision floating-point number equivalent. A return value indicates whether the conversion succeeded or failed. /// @@ -43,7 +43,7 @@ public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatPro /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out double result) => - double.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + double.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/net472/Numbers/SinglePolyfill.cs b/src/Split/net472/Numbers/SinglePolyfill.cs index 69af78095..2f6e02a82 100644 --- a/src/Split/net472/Numbers/SinglePolyfill.cs +++ b/src/Split/net472/Numbers/SinglePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out float result) => - float.TryParse(s, NumberStyles.Float, provider, out result); + float.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,7 +28,7 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its single-precision floating-point number equivalent. /// public static bool TryParse(ReadOnlySpan utf8Text, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Converts the span representation of a number in a specified style and culture-specific format to its single-precision floating-point number equivalent. A return value indicates whether the conversion succeeded or failed. /// @@ -43,7 +43,7 @@ public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatPro /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out float result) => - float.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + float.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/net472/Polyfill.cs b/src/Split/net472/Polyfill.cs index 16e916262..b7b21af17 100644 --- a/src/Split/net472/Polyfill.cs +++ b/src/Split/net472/Polyfill.cs @@ -17,5 +17,5 @@ namespace Polyfills; #endif static partial class Polyfill { - const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond * 1000; + const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; } diff --git a/src/Split/net472/Polyfill_ArraySegment.cs b/src/Split/net472/Polyfill_ArraySegment.cs index 96e83c18d..05ea1f659 100644 --- a/src/Split/net472/Polyfill_ArraySegment.cs +++ b/src/Split/net472/Polyfill_ArraySegment.cs @@ -12,15 +12,19 @@ static partial class Polyfill /// public static void CopyTo(this ArraySegment target, ArraySegment destination) { - if (target.Count > destination.Count) + if (target.Array == null) { - throw new ArgumentException("DestinationTooShort", nameof(destination)); + throw new InvalidOperationException("InvalidOperation_NullArray"); } - if (target.Array == null) + if (destination.Array == null) { throw new InvalidOperationException("InvalidOperation_NullArray"); } - Array.Copy(target.Array!, target.Offset, destination.Array, destination.Offset, target.Count); + if (target.Count > destination.Count) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } + Array.Copy(target.Array, target.Offset, destination.Array, destination.Offset, target.Count); } /// /// Copies the contents of this instance into the specified destination array of the same type T. diff --git a/src/Split/net472/Polyfill_Encoding_GetChars.cs b/src/Split/net472/Polyfill_Encoding_GetChars.cs index f5a7de80b..4b956e845 100644 --- a/src/Split/net472/Polyfill_Encoding_GetChars.cs +++ b/src/Split/net472/Polyfill_Encoding_GetChars.cs @@ -25,7 +25,7 @@ public static int GetChars(this Encoding target, ReadOnlySpan bytes, Span< var charArray = new char[bytes.Length]; var array = bytes.ToArray(); var count = target.GetChars(array, 0, bytes.Length, charArray, 0); - new ReadOnlySpan(charArray).CopyTo(chars); + new ReadOnlySpan(charArray, 0, count).CopyTo(chars); return count; } #endif diff --git a/src/Split/net472/Polyfill_List.cs b/src/Split/net472/Polyfill_List.cs index c2e76ed77..7a9348d0c 100644 --- a/src/Split/net472/Polyfill_List.cs +++ b/src/Split/net472/Polyfill_List.cs @@ -21,6 +21,10 @@ public static void AddRange(this List target, ReadOnlySpan source) /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { + if ((uint)index > (uint)target.Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } for (var i = 0; i < source.Length; i++) { var item = source[i]; @@ -30,6 +34,10 @@ public static void InsertRange(this List target, int index, ReadOnlySpanCopies the entire to a span. public static void CopyTo(this List target, Span destination) { + if (target.Count > destination.Length) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } for (var index = 0; index < target.Count; index++) { destination[index] = target[index]; diff --git a/src/Split/net472/Polyfill_Memory_SpanSort.cs b/src/Split/net472/Polyfill_Memory_SpanSort.cs index acc5c8666..b30957972 100644 --- a/src/Split/net472/Polyfill_Memory_SpanSort.cs +++ b/src/Split/net472/Polyfill_Memory_SpanSort.cs @@ -77,12 +77,8 @@ internal ComparerWrapper(Comparison comparison) throw new ArgumentNullException(nameof(comparison)); this.comparison = comparison; } - public int Compare(T? x, T? y) - { - if (x is null || y is null) - return 0; - return comparison((T)x, (T)y); - } + public int Compare(T? x, T? y) => + comparison(x!, y!); } } #endif diff --git a/src/Split/net472/Polyfill_MicroNanosecondAdd.cs b/src/Split/net472/Polyfill_MicroNanosecondAdd.cs index 17d2c8d00..e099e1260 100644 --- a/src/Split/net472/Polyfill_MicroNanosecondAdd.cs +++ b/src/Split/net472/Polyfill_MicroNanosecondAdd.cs @@ -8,10 +8,10 @@ static partial class Polyfill /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTime AddMicroseconds(this DateTime target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); /// /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTimeOffset AddMicroseconds(this DateTimeOffset target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); } diff --git a/src/Split/net472/Polyfill_String.cs b/src/Split/net472/Polyfill_String.cs index bab55956e..254f4be0a 100644 --- a/src/Split/net472/Polyfill_String.cs +++ b/src/Split/net472/Polyfill_String.cs @@ -3,8 +3,6 @@ namespace Polyfills; using System; using System.Text; -using System.IO; -using System.Runtime.CompilerServices; static partial class Polyfill { #if FeatureMemory @@ -147,22 +145,49 @@ public static int LastIndexOf(this string target, char value, int startIndex, in /// /// Replaces all newline sequences in the current string with . /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ReplaceLineEndings(this string target, string replacementText) { + var index = IndexOfNewlineChar(target, 0, out var stride); + if (index < 0) + { + return target; + } var builder = new StringBuilder(target.Length); - using var reader = new StringReader(target); + var position = 0; while (true) { - var line = reader.ReadLine(); - if (line == null) + builder.Append(target, position, index - position); + builder.Append(replacementText); + position = index + stride; + index = IndexOfNewlineChar(target, position, out stride); + if (index < 0) { break; } - builder.Append(line); - builder.Append(replacementText); } - return builder.ToString(0, builder.Length - replacementText.Length); + builder.Append(target, position, target.Length - position); + return builder.ToString(); + } + static int IndexOfNewlineChar(string text, int startIndex, out int stride) + { + stride = 0; + for (var index = startIndex; index < text.Length; index++) + { + switch (text[index]) + { + case '\r': + stride = index + 1 < text.Length && text[index + 1] == '\n' ? 2 : 1; + return index; + case '\n': + case '\f': + case '\u0085': + case '\u2028': + case '\u2029': + stride = 1; + return index; + } + } + return -1; } /// /// Replaces all newline sequences in the current string with . diff --git a/src/Split/net472/Polyfill_StringBuilder_Append.cs b/src/Split/net472/Polyfill_StringBuilder_Append.cs index 0b60749a9..d3970ce53 100644 --- a/src/Split/net472/Polyfill_StringBuilder_Append.cs +++ b/src/Split/net472/Polyfill_StringBuilder_Append.cs @@ -10,6 +10,14 @@ static partial class Polyfill /// public static StringBuilder Append(this StringBuilder target, StringBuilder? value, int startIndex, int count) { + if (startIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(startIndex)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } if (value == null) { if (startIndex == 0 && count == 0) diff --git a/src/Split/net472/Polyfill_StringBuilder_CopyTo.cs b/src/Split/net472/Polyfill_StringBuilder_CopyTo.cs index dc625a7bc..ba99e0a1f 100644 --- a/src/Split/net472/Polyfill_StringBuilder_CopyTo.cs +++ b/src/Split/net472/Polyfill_StringBuilder_CopyTo.cs @@ -15,20 +15,25 @@ public static void CopyTo( Span destination, int count) { - var destinationIndex = 0; - while (true) + if (count < 0) { - if (sourceIndex == target.Length) - { - break; - } - if (destinationIndex == count) - { - break; - } - destination[destinationIndex] = target[sourceIndex]; - destinationIndex++; - sourceIndex++; + throw new ArgumentOutOfRangeException(nameof(count)); + } + if ((uint)sourceIndex > (uint)target.Length) + { + throw new ArgumentOutOfRangeException(nameof(sourceIndex)); + } + if (sourceIndex > target.Length - count) + { + throw new ArgumentException("The source StringBuilder does not contain enough characters from sourceIndex onward to satisfy count."); + } + if (count > destination.Length) + { + throw new ArgumentException("The destination span is too short to hold the requested characters.", nameof(destination)); + } + for (var index = 0; index < count; index++) + { + destination[index] = target[sourceIndex + index]; } } } diff --git a/src/Split/net472/Polyfill_Type.cs b/src/Split/net472/Polyfill_Type.cs index 7890e2168..b99bdfab5 100644 --- a/src/Split/net472/Polyfill_Type.cs +++ b/src/Split/net472/Polyfill_Type.cs @@ -7,9 +7,15 @@ namespace Polyfills; using System.Linq; static partial class Polyfill { - public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInfo other) => - target.MetadataToken == other.MetadataToken && - target.Module.Equals(other.Module); + public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInfo other) + { + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } + return target.MetadataToken == other.MetadataToken && + target.Module.Equals(other.Module); + } /// /// Searches for the specified method whose parameters match the specified generic parameter count, argument types and modifiers, using the specified binding constraints. /// @@ -22,6 +28,25 @@ public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInf Type[] types, ParameterModifier[]? modifiers) { + if (name is null) + { + throw new ArgumentNullException(nameof(name)); + } + if (genericParameterCount < 0) + { + throw new ArgumentOutOfRangeException(nameof(genericParameterCount)); + } + if (types is null) + { + throw new ArgumentNullException(nameof(types)); + } + foreach (var type in types) + { + if (type is null) + { + throw new ArgumentNullException(nameof(types)); + } + } var methods = target.GetMethods(bindingAttr); if (genericParameterCount == 0) { diff --git a/src/Split/net472/RandomNumberGeneratorPolyfill.cs b/src/Split/net472/RandomNumberGeneratorPolyfill.cs index 08384a7ea..539aa7aba 100644 --- a/src/Split/net472/RandomNumberGeneratorPolyfill.cs +++ b/src/Split/net472/RandomNumberGeneratorPolyfill.cs @@ -34,7 +34,7 @@ public static int GetInt32(int fromInclusive, int toExclusive) { generator.GetBytes(bytes); value = BitConverter.ToUInt32(bytes, 0) & mask; - } while (value >= range); + } while (value > range); return (int) (fromInclusive + value); } /// diff --git a/src/Split/net472/StringPolyfill.cs b/src/Split/net472/StringPolyfill.cs index bbdee2733..b14512f1d 100644 --- a/src/Split/net472/StringPolyfill.cs +++ b/src/Split/net472/StringPolyfill.cs @@ -50,6 +50,10 @@ public static string Join(char separator, scoped ReadOnlySpan values) span = span.Slice(1); } var value = values[index]; + if (value is null) + { + continue; + } value.CopyTo(span); span = span.Slice(value.Length); } diff --git a/src/Split/net472/TaskCompletionSource.cs b/src/Split/net472/TaskCompletionSource.cs index 04b507f76..67a35f938 100644 --- a/src/Split/net472/TaskCompletionSource.cs +++ b/src/Split/net472/TaskCompletionSource.cs @@ -77,5 +77,5 @@ public TaskCompletionSource(object? state, TaskCreationOptions creationOptions) /// /// Attempts to transition the underlying into the state. /// - public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(default); + public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(cancellationToken); } diff --git a/src/Split/net472/TimeSpanPolyfill.cs b/src/Split/net472/TimeSpanPolyfill.cs index 94d13cbbb..59d7c7974 100644 --- a/src/Split/net472/TimeSpanPolyfill.cs +++ b/src/Split/net472/TimeSpanPolyfill.cs @@ -15,7 +15,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microseconds => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new TimeSpan(target.Days, target.Hours, target.Minutes, 0); @@ -24,52 +24,71 @@ long TicksComponent() } } const long MicrosecondsToTicks = TimeSpan.TicksPerMillisecond / 1000; + const long MicrosecondsPerMillisecond = 1_000; + const long MicrosecondsPerSecond = 1_000_000; + const long MicrosecondsPerMinute = 60_000_000; + const long MicrosecondsPerHour = 3_600_000_000; + const long MicrosecondsPerDay = 86_400_000_000; + static TimeSpan TicksFromMicroseconds(decimal totalMicroseconds) + { + var ticks = totalMicroseconds * MicrosecondsToTicks; + if (ticks < long.MinValue || ticks > long.MaxValue) + { + throw new ArgumentOutOfRangeException(null, "TimeSpan overflowed because the duration is too long."); + } + return new((long)ticks); + } extension(TimeSpan) { /// /// Initializes a new instance of the structure to a specified number of days, hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromDays(int days, int hours = 0, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)days * TimeSpan.TicksPerDay + - (long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)days * MicrosecondsPerDay + + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromHours(int hours, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromMinutes(int minutes, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of seconds, milliseconds, and microseconds. /// public static TimeSpan FromSeconds(long seconds, long milliseconds = 0, long microseconds = 0) => - new(seconds * TimeSpan.TicksPerSecond + - milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of milliseconds and microseconds. /// public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0) => - new(milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of microseconds. /// public static TimeSpan FromMicroseconds(long microseconds) => - new(microseconds * MicrosecondsToTicks); + TicksFromMicroseconds(microseconds); } } diff --git a/src/Split/net472/XDocumentPolyfill.cs b/src/Split/net472/XDocumentPolyfill.cs index 1611acf19..876842223 100644 --- a/src/Split/net472/XDocumentPolyfill.cs +++ b/src/Split/net472/XDocumentPolyfill.cs @@ -25,12 +25,10 @@ static partial class XDocumentPolyfill /// /// Asynchronously creates a new XDocument and initializes its underlying XML tree using the specified stream, optionally preserving white space. /// - public static async Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) + public static Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - using var reader = new StreamReader(stream); - var content = await reader.ReadToEndAsync(); - return XDocument.Parse(content, options); + return Task.FromResult(XDocument.Load(stream, options)); } /// /// Asynchronously creates a new XDocument and initializes its underlying XML tree using the specified text reader, optionally preserving white space. diff --git a/src/Split/net472/XElementPolyfill.cs b/src/Split/net472/XElementPolyfill.cs index 27208879a..9347d9cdd 100644 --- a/src/Split/net472/XElementPolyfill.cs +++ b/src/Split/net472/XElementPolyfill.cs @@ -25,12 +25,10 @@ static partial class XElementPolyfill /// /// Asynchronously creates a new XElement and initializes its underlying XML tree using the specified stream, optionally preserving white space. /// - public static async Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) + public static Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - using var reader = new StreamReader(stream); - var content = await reader.ReadToEndAsync(); - return XElement.Parse(content, options); + return Task.FromResult(XElement.Load(stream, options)); } /// /// Asynchronously creates a new XElement and initializes its underlying XML tree using the specified text reader, optionally preserving white space. diff --git a/src/Split/net472/ZipFilePolyfill.cs b/src/Split/net472/ZipFilePolyfill.cs index 6cb361e6f..d908c5633 100644 --- a/src/Split/net472/ZipFilePolyfill.cs +++ b/src/Split/net472/ZipFilePolyfill.cs @@ -53,11 +53,12 @@ static void ExtractToDirectoryPolyfill( string destinationDirectory, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + return; } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, null); } static void ExtractToDirectoryPolyfill( string sourceArchiveFile, @@ -65,11 +66,43 @@ static void ExtractToDirectoryPolyfill( Encoding encoding, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); + return; + } + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, encoding); + } + static void ExtractToDirectoryWithOverwrite( + string sourceArchiveFile, + string destinationDirectory, + Encoding? encoding) + { + var destinationRoot = Path.GetFullPath(destinationDirectory); + Directory.CreateDirectory(destinationRoot); + var normalizedRoot = destinationRoot; + if (normalizedRoot[normalizedRoot.Length - 1] != Path.DirectorySeparatorChar) + { + normalizedRoot += Path.DirectorySeparatorChar; + } + using var archive = encoding == null + ? ZipFile.OpenRead(sourceArchiveFile) + : ZipFile.Open(sourceArchiveFile, ZipArchiveMode.Read, encoding); + foreach (var entry in archive.Entries) + { + var destinationPath = Path.GetFullPath(Path.Combine(destinationRoot, entry.FullName)); + if (!destinationPath.StartsWith(normalizedRoot, StringComparison.Ordinal)) + { + throw new IOException("Extracting Zip entry would have resulted in a file outside the specified destination directory."); + } + if (entry.Name.Length == 0) + { + Directory.CreateDirectory(destinationPath); + continue; + } + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + entry.ExtractToFile(destinationPath, overwrite: true); } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); } /// /// Asynchronously opens a ZipArchive on the specified archiveFileName in the specified ZipArchiveMode mode. diff --git a/src/Split/net48/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs b/src/Split/net48/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs index 32b0afded..8bad1d83d 100644 --- a/src/Split/net48/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs +++ b/src/Split/net48/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs @@ -15,7 +15,7 @@ public static void ThrowIfZero(T value, [CallerArgumentExpression(nameof(valu { if (value.Equals(default)) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } /// Throws an if is zero. @@ -23,12 +23,12 @@ public static void ThrowIfZero(nint value, [CallerArgumentExpression(nameof(valu { if (value == 0) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } [DoesNotReturn] - static void ThrowZero(string? paramName) => - throw new ArgumentOutOfRangeException(paramName, "Value must not be zero."); + static void ThrowZero(T value, string? paramName) => + throw new ArgumentOutOfRangeException(paramName, value, $"{paramName} ('{value}') must be a non-zero value."); public static void ThrowIfNegative(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : struct, IComparable { @@ -112,7 +112,7 @@ public static void ThrowIfGreaterThanOrEqual(nint value, nint other, [CallerArgu { if (value >= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than {other}."); } } public static void ThrowIfLessThan(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) @@ -144,7 +144,7 @@ public static void ThrowIfLessThanOrEqual(nint value, nint other, [CallerArgumen { if (value <= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than {other}."); } } } diff --git a/src/Split/net48/ConvertPolyfill.cs b/src/Split/net48/ConvertPolyfill.cs index 412674710..1f45391f9 100644 --- a/src/Split/net48/ConvertPolyfill.cs +++ b/src/Split/net48/ConvertPolyfill.cs @@ -134,19 +134,19 @@ public static OperationStatus FromHexString(ReadOnlySpan source, Span= destination.Length) { charsConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal(source[i]); + var lo = GetHexVal(source[i + 1]); + if (hi < 0 || lo < 0) { - charsConsumed = i; + charsConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; @@ -193,19 +193,19 @@ public static OperationStatus FromHexString(ReadOnlySpan utf8Source, Span< var j = 0; while (i + 1 < utf8Source.Length) { - var hi = GetHexVal((char)utf8Source[i]); - var lo = GetHexVal((char)utf8Source[i + 1]); - if (hi < 0 || lo < 0) + if (j >= destination.Length) { bytesConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal((char)utf8Source[i]); + var lo = GetHexVal((char)utf8Source[i + 1]); + if (hi < 0 || lo < 0) { - bytesConsumed = i; + bytesConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; diff --git a/src/Split/net48/DateTimeOffsetPolyfill.cs b/src/Split/net48/DateTimeOffsetPolyfill.cs index 6837790e5..012b944dc 100644 --- a/src/Split/net48/DateTimeOffsetPolyfill.cs +++ b/src/Split/net48/DateTimeOffsetPolyfill.cs @@ -17,7 +17,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTimeOffset(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Offset); diff --git a/src/Split/net48/DateTimePolyfill.cs b/src/Split/net48/DateTimePolyfill.cs index 4641f3e59..430683e14 100644 --- a/src/Split/net48/DateTimePolyfill.cs +++ b/src/Split/net48/DateTimePolyfill.cs @@ -16,7 +16,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTime(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Kind); diff --git a/src/Split/net48/EqualityComparerPolyfill.cs b/src/Split/net48/EqualityComparerPolyfill.cs index e45f2a30e..55cb3ad44 100644 --- a/src/Split/net48/EqualityComparerPolyfill.cs +++ b/src/Split/net48/EqualityComparerPolyfill.cs @@ -19,8 +19,14 @@ public override int GetHashCode(T obj) => /// public static EqualityComparer Create( Func equals, - Func? getHashCode = null) => - new DelegateEqualityComparer(equals, getHashCode); + Func? getHashCode = null) + { + if (equals is null) + { + throw new ArgumentNullException(nameof(equals)); + } + return new DelegateEqualityComparer(equals, getHashCode); + } } class KeySelectorEqualityComparer(Func keySelector, IEqualityComparer? keyComparer) : EqualityComparer @@ -41,7 +47,13 @@ public override int GetHashCode(T obj) /// public static EqualityComparer Create( Func keySelector, - IEqualityComparer? keyComparer = null) => - new KeySelectorEqualityComparer(keySelector, keyComparer); + IEqualityComparer? keyComparer = null) + { + if (keySelector is null) + { + throw new ArgumentNullException(nameof(keySelector)); + } + return new KeySelectorEqualityComparer(keySelector, keyComparer); + } } } diff --git a/src/Split/net48/FileUnixModePolyfill.cs b/src/Split/net48/FileUnixModePolyfill.cs index 2440f7800..701ef07cb 100644 --- a/src/Split/net48/FileUnixModePolyfill.cs +++ b/src/Split/net48/FileUnixModePolyfill.cs @@ -47,8 +47,8 @@ public static UnixFileMode GetUnixFileMode(string path) var mode = output[0] switch { '1' => UnixFileMode.StickyBit, - '2' => UnixFileMode.SetUser, - '4' => UnixFileMode.SetGroup, + '2' => UnixFileMode.SetGroup, + '4' => UnixFileMode.SetUser, _ => UnixFileMode.None }; mode = output[1] switch @@ -158,13 +158,13 @@ public static void SetUnixFileMode(string path, UnixFileMode mode) octal[3] += 1; break; case "setuser": - octal[0] += 2; + octal[0] += 4; break; case "stickybit": octal[0] += 1; break; case "setgroup": - octal[0] += 4; + octal[0] += 2; break; default: throw new("Invalid notation detected"); diff --git a/src/Split/net48/Numbers/DoublePolyfill.cs b/src/Split/net48/Numbers/DoublePolyfill.cs index ba5333f69..2e3e40efd 100644 --- a/src/Split/net48/Numbers/DoublePolyfill.cs +++ b/src/Split/net48/Numbers/DoublePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out double result) => - double.TryParse(s, NumberStyles.Float, provider, out result); + double.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,7 +28,7 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its double-precision floating-point number equivalent.. /// public static bool TryParse(ReadOnlySpan utf8Text, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Converts the span representation of a number in a specified style and culture-specific format to its double-precision floating-point number equivalent. A return value indicates whether the conversion succeeded or failed. /// @@ -43,7 +43,7 @@ public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatPro /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out double result) => - double.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + double.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/net48/Numbers/SinglePolyfill.cs b/src/Split/net48/Numbers/SinglePolyfill.cs index 69af78095..2f6e02a82 100644 --- a/src/Split/net48/Numbers/SinglePolyfill.cs +++ b/src/Split/net48/Numbers/SinglePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out float result) => - float.TryParse(s, NumberStyles.Float, provider, out result); + float.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,7 +28,7 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its single-precision floating-point number equivalent. /// public static bool TryParse(ReadOnlySpan utf8Text, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Converts the span representation of a number in a specified style and culture-specific format to its single-precision floating-point number equivalent. A return value indicates whether the conversion succeeded or failed. /// @@ -43,7 +43,7 @@ public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatPro /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out float result) => - float.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + float.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/net48/Polyfill.cs b/src/Split/net48/Polyfill.cs index 16e916262..b7b21af17 100644 --- a/src/Split/net48/Polyfill.cs +++ b/src/Split/net48/Polyfill.cs @@ -17,5 +17,5 @@ namespace Polyfills; #endif static partial class Polyfill { - const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond * 1000; + const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; } diff --git a/src/Split/net48/Polyfill_ArraySegment.cs b/src/Split/net48/Polyfill_ArraySegment.cs index 96e83c18d..05ea1f659 100644 --- a/src/Split/net48/Polyfill_ArraySegment.cs +++ b/src/Split/net48/Polyfill_ArraySegment.cs @@ -12,15 +12,19 @@ static partial class Polyfill /// public static void CopyTo(this ArraySegment target, ArraySegment destination) { - if (target.Count > destination.Count) + if (target.Array == null) { - throw new ArgumentException("DestinationTooShort", nameof(destination)); + throw new InvalidOperationException("InvalidOperation_NullArray"); } - if (target.Array == null) + if (destination.Array == null) { throw new InvalidOperationException("InvalidOperation_NullArray"); } - Array.Copy(target.Array!, target.Offset, destination.Array, destination.Offset, target.Count); + if (target.Count > destination.Count) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } + Array.Copy(target.Array, target.Offset, destination.Array, destination.Offset, target.Count); } /// /// Copies the contents of this instance into the specified destination array of the same type T. diff --git a/src/Split/net48/Polyfill_Encoding_GetChars.cs b/src/Split/net48/Polyfill_Encoding_GetChars.cs index f5a7de80b..4b956e845 100644 --- a/src/Split/net48/Polyfill_Encoding_GetChars.cs +++ b/src/Split/net48/Polyfill_Encoding_GetChars.cs @@ -25,7 +25,7 @@ public static int GetChars(this Encoding target, ReadOnlySpan bytes, Span< var charArray = new char[bytes.Length]; var array = bytes.ToArray(); var count = target.GetChars(array, 0, bytes.Length, charArray, 0); - new ReadOnlySpan(charArray).CopyTo(chars); + new ReadOnlySpan(charArray, 0, count).CopyTo(chars); return count; } #endif diff --git a/src/Split/net48/Polyfill_List.cs b/src/Split/net48/Polyfill_List.cs index c2e76ed77..7a9348d0c 100644 --- a/src/Split/net48/Polyfill_List.cs +++ b/src/Split/net48/Polyfill_List.cs @@ -21,6 +21,10 @@ public static void AddRange(this List target, ReadOnlySpan source) /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { + if ((uint)index > (uint)target.Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } for (var i = 0; i < source.Length; i++) { var item = source[i]; @@ -30,6 +34,10 @@ public static void InsertRange(this List target, int index, ReadOnlySpanCopies the entire to a span. public static void CopyTo(this List target, Span destination) { + if (target.Count > destination.Length) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } for (var index = 0; index < target.Count; index++) { destination[index] = target[index]; diff --git a/src/Split/net48/Polyfill_Memory_SpanSort.cs b/src/Split/net48/Polyfill_Memory_SpanSort.cs index acc5c8666..b30957972 100644 --- a/src/Split/net48/Polyfill_Memory_SpanSort.cs +++ b/src/Split/net48/Polyfill_Memory_SpanSort.cs @@ -77,12 +77,8 @@ internal ComparerWrapper(Comparison comparison) throw new ArgumentNullException(nameof(comparison)); this.comparison = comparison; } - public int Compare(T? x, T? y) - { - if (x is null || y is null) - return 0; - return comparison((T)x, (T)y); - } + public int Compare(T? x, T? y) => + comparison(x!, y!); } } #endif diff --git a/src/Split/net48/Polyfill_MicroNanosecondAdd.cs b/src/Split/net48/Polyfill_MicroNanosecondAdd.cs index 17d2c8d00..e099e1260 100644 --- a/src/Split/net48/Polyfill_MicroNanosecondAdd.cs +++ b/src/Split/net48/Polyfill_MicroNanosecondAdd.cs @@ -8,10 +8,10 @@ static partial class Polyfill /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTime AddMicroseconds(this DateTime target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); /// /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTimeOffset AddMicroseconds(this DateTimeOffset target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); } diff --git a/src/Split/net48/Polyfill_String.cs b/src/Split/net48/Polyfill_String.cs index bab55956e..254f4be0a 100644 --- a/src/Split/net48/Polyfill_String.cs +++ b/src/Split/net48/Polyfill_String.cs @@ -3,8 +3,6 @@ namespace Polyfills; using System; using System.Text; -using System.IO; -using System.Runtime.CompilerServices; static partial class Polyfill { #if FeatureMemory @@ -147,22 +145,49 @@ public static int LastIndexOf(this string target, char value, int startIndex, in /// /// Replaces all newline sequences in the current string with . /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ReplaceLineEndings(this string target, string replacementText) { + var index = IndexOfNewlineChar(target, 0, out var stride); + if (index < 0) + { + return target; + } var builder = new StringBuilder(target.Length); - using var reader = new StringReader(target); + var position = 0; while (true) { - var line = reader.ReadLine(); - if (line == null) + builder.Append(target, position, index - position); + builder.Append(replacementText); + position = index + stride; + index = IndexOfNewlineChar(target, position, out stride); + if (index < 0) { break; } - builder.Append(line); - builder.Append(replacementText); } - return builder.ToString(0, builder.Length - replacementText.Length); + builder.Append(target, position, target.Length - position); + return builder.ToString(); + } + static int IndexOfNewlineChar(string text, int startIndex, out int stride) + { + stride = 0; + for (var index = startIndex; index < text.Length; index++) + { + switch (text[index]) + { + case '\r': + stride = index + 1 < text.Length && text[index + 1] == '\n' ? 2 : 1; + return index; + case '\n': + case '\f': + case '\u0085': + case '\u2028': + case '\u2029': + stride = 1; + return index; + } + } + return -1; } /// /// Replaces all newline sequences in the current string with . diff --git a/src/Split/net48/Polyfill_StringBuilder_Append.cs b/src/Split/net48/Polyfill_StringBuilder_Append.cs index 0b60749a9..d3970ce53 100644 --- a/src/Split/net48/Polyfill_StringBuilder_Append.cs +++ b/src/Split/net48/Polyfill_StringBuilder_Append.cs @@ -10,6 +10,14 @@ static partial class Polyfill /// public static StringBuilder Append(this StringBuilder target, StringBuilder? value, int startIndex, int count) { + if (startIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(startIndex)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } if (value == null) { if (startIndex == 0 && count == 0) diff --git a/src/Split/net48/Polyfill_StringBuilder_CopyTo.cs b/src/Split/net48/Polyfill_StringBuilder_CopyTo.cs index dc625a7bc..ba99e0a1f 100644 --- a/src/Split/net48/Polyfill_StringBuilder_CopyTo.cs +++ b/src/Split/net48/Polyfill_StringBuilder_CopyTo.cs @@ -15,20 +15,25 @@ public static void CopyTo( Span destination, int count) { - var destinationIndex = 0; - while (true) + if (count < 0) { - if (sourceIndex == target.Length) - { - break; - } - if (destinationIndex == count) - { - break; - } - destination[destinationIndex] = target[sourceIndex]; - destinationIndex++; - sourceIndex++; + throw new ArgumentOutOfRangeException(nameof(count)); + } + if ((uint)sourceIndex > (uint)target.Length) + { + throw new ArgumentOutOfRangeException(nameof(sourceIndex)); + } + if (sourceIndex > target.Length - count) + { + throw new ArgumentException("The source StringBuilder does not contain enough characters from sourceIndex onward to satisfy count."); + } + if (count > destination.Length) + { + throw new ArgumentException("The destination span is too short to hold the requested characters.", nameof(destination)); + } + for (var index = 0; index < count; index++) + { + destination[index] = target[sourceIndex + index]; } } } diff --git a/src/Split/net48/Polyfill_Type.cs b/src/Split/net48/Polyfill_Type.cs index 7890e2168..b99bdfab5 100644 --- a/src/Split/net48/Polyfill_Type.cs +++ b/src/Split/net48/Polyfill_Type.cs @@ -7,9 +7,15 @@ namespace Polyfills; using System.Linq; static partial class Polyfill { - public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInfo other) => - target.MetadataToken == other.MetadataToken && - target.Module.Equals(other.Module); + public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInfo other) + { + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } + return target.MetadataToken == other.MetadataToken && + target.Module.Equals(other.Module); + } /// /// Searches for the specified method whose parameters match the specified generic parameter count, argument types and modifiers, using the specified binding constraints. /// @@ -22,6 +28,25 @@ public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInf Type[] types, ParameterModifier[]? modifiers) { + if (name is null) + { + throw new ArgumentNullException(nameof(name)); + } + if (genericParameterCount < 0) + { + throw new ArgumentOutOfRangeException(nameof(genericParameterCount)); + } + if (types is null) + { + throw new ArgumentNullException(nameof(types)); + } + foreach (var type in types) + { + if (type is null) + { + throw new ArgumentNullException(nameof(types)); + } + } var methods = target.GetMethods(bindingAttr); if (genericParameterCount == 0) { diff --git a/src/Split/net48/RandomNumberGeneratorPolyfill.cs b/src/Split/net48/RandomNumberGeneratorPolyfill.cs index 08384a7ea..539aa7aba 100644 --- a/src/Split/net48/RandomNumberGeneratorPolyfill.cs +++ b/src/Split/net48/RandomNumberGeneratorPolyfill.cs @@ -34,7 +34,7 @@ public static int GetInt32(int fromInclusive, int toExclusive) { generator.GetBytes(bytes); value = BitConverter.ToUInt32(bytes, 0) & mask; - } while (value >= range); + } while (value > range); return (int) (fromInclusive + value); } /// diff --git a/src/Split/net48/StringPolyfill.cs b/src/Split/net48/StringPolyfill.cs index bbdee2733..b14512f1d 100644 --- a/src/Split/net48/StringPolyfill.cs +++ b/src/Split/net48/StringPolyfill.cs @@ -50,6 +50,10 @@ public static string Join(char separator, scoped ReadOnlySpan values) span = span.Slice(1); } var value = values[index]; + if (value is null) + { + continue; + } value.CopyTo(span); span = span.Slice(value.Length); } diff --git a/src/Split/net48/TaskCompletionSource.cs b/src/Split/net48/TaskCompletionSource.cs index 04b507f76..67a35f938 100644 --- a/src/Split/net48/TaskCompletionSource.cs +++ b/src/Split/net48/TaskCompletionSource.cs @@ -77,5 +77,5 @@ public TaskCompletionSource(object? state, TaskCreationOptions creationOptions) /// /// Attempts to transition the underlying into the state. /// - public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(default); + public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(cancellationToken); } diff --git a/src/Split/net48/TimeSpanPolyfill.cs b/src/Split/net48/TimeSpanPolyfill.cs index 94d13cbbb..59d7c7974 100644 --- a/src/Split/net48/TimeSpanPolyfill.cs +++ b/src/Split/net48/TimeSpanPolyfill.cs @@ -15,7 +15,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microseconds => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new TimeSpan(target.Days, target.Hours, target.Minutes, 0); @@ -24,52 +24,71 @@ long TicksComponent() } } const long MicrosecondsToTicks = TimeSpan.TicksPerMillisecond / 1000; + const long MicrosecondsPerMillisecond = 1_000; + const long MicrosecondsPerSecond = 1_000_000; + const long MicrosecondsPerMinute = 60_000_000; + const long MicrosecondsPerHour = 3_600_000_000; + const long MicrosecondsPerDay = 86_400_000_000; + static TimeSpan TicksFromMicroseconds(decimal totalMicroseconds) + { + var ticks = totalMicroseconds * MicrosecondsToTicks; + if (ticks < long.MinValue || ticks > long.MaxValue) + { + throw new ArgumentOutOfRangeException(null, "TimeSpan overflowed because the duration is too long."); + } + return new((long)ticks); + } extension(TimeSpan) { /// /// Initializes a new instance of the structure to a specified number of days, hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromDays(int days, int hours = 0, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)days * TimeSpan.TicksPerDay + - (long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)days * MicrosecondsPerDay + + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromHours(int hours, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromMinutes(int minutes, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of seconds, milliseconds, and microseconds. /// public static TimeSpan FromSeconds(long seconds, long milliseconds = 0, long microseconds = 0) => - new(seconds * TimeSpan.TicksPerSecond + - milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of milliseconds and microseconds. /// public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0) => - new(milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of microseconds. /// public static TimeSpan FromMicroseconds(long microseconds) => - new(microseconds * MicrosecondsToTicks); + TicksFromMicroseconds(microseconds); } } diff --git a/src/Split/net48/XDocumentPolyfill.cs b/src/Split/net48/XDocumentPolyfill.cs index 1611acf19..876842223 100644 --- a/src/Split/net48/XDocumentPolyfill.cs +++ b/src/Split/net48/XDocumentPolyfill.cs @@ -25,12 +25,10 @@ static partial class XDocumentPolyfill /// /// Asynchronously creates a new XDocument and initializes its underlying XML tree using the specified stream, optionally preserving white space. /// - public static async Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) + public static Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - using var reader = new StreamReader(stream); - var content = await reader.ReadToEndAsync(); - return XDocument.Parse(content, options); + return Task.FromResult(XDocument.Load(stream, options)); } /// /// Asynchronously creates a new XDocument and initializes its underlying XML tree using the specified text reader, optionally preserving white space. diff --git a/src/Split/net48/XElementPolyfill.cs b/src/Split/net48/XElementPolyfill.cs index 27208879a..9347d9cdd 100644 --- a/src/Split/net48/XElementPolyfill.cs +++ b/src/Split/net48/XElementPolyfill.cs @@ -25,12 +25,10 @@ static partial class XElementPolyfill /// /// Asynchronously creates a new XElement and initializes its underlying XML tree using the specified stream, optionally preserving white space. /// - public static async Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) + public static Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - using var reader = new StreamReader(stream); - var content = await reader.ReadToEndAsync(); - return XElement.Parse(content, options); + return Task.FromResult(XElement.Load(stream, options)); } /// /// Asynchronously creates a new XElement and initializes its underlying XML tree using the specified text reader, optionally preserving white space. diff --git a/src/Split/net48/ZipFilePolyfill.cs b/src/Split/net48/ZipFilePolyfill.cs index 6cb361e6f..d908c5633 100644 --- a/src/Split/net48/ZipFilePolyfill.cs +++ b/src/Split/net48/ZipFilePolyfill.cs @@ -53,11 +53,12 @@ static void ExtractToDirectoryPolyfill( string destinationDirectory, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + return; } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, null); } static void ExtractToDirectoryPolyfill( string sourceArchiveFile, @@ -65,11 +66,43 @@ static void ExtractToDirectoryPolyfill( Encoding encoding, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); + return; + } + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, encoding); + } + static void ExtractToDirectoryWithOverwrite( + string sourceArchiveFile, + string destinationDirectory, + Encoding? encoding) + { + var destinationRoot = Path.GetFullPath(destinationDirectory); + Directory.CreateDirectory(destinationRoot); + var normalizedRoot = destinationRoot; + if (normalizedRoot[normalizedRoot.Length - 1] != Path.DirectorySeparatorChar) + { + normalizedRoot += Path.DirectorySeparatorChar; + } + using var archive = encoding == null + ? ZipFile.OpenRead(sourceArchiveFile) + : ZipFile.Open(sourceArchiveFile, ZipArchiveMode.Read, encoding); + foreach (var entry in archive.Entries) + { + var destinationPath = Path.GetFullPath(Path.Combine(destinationRoot, entry.FullName)); + if (!destinationPath.StartsWith(normalizedRoot, StringComparison.Ordinal)) + { + throw new IOException("Extracting Zip entry would have resulted in a file outside the specified destination directory."); + } + if (entry.Name.Length == 0) + { + Directory.CreateDirectory(destinationPath); + continue; + } + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + entry.ExtractToFile(destinationPath, overwrite: true); } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); } /// /// Asynchronously opens a ZipArchive on the specified archiveFileName in the specified ZipArchiveMode mode. diff --git a/src/Split/net481/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs b/src/Split/net481/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs index 32b0afded..8bad1d83d 100644 --- a/src/Split/net481/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs +++ b/src/Split/net481/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs @@ -15,7 +15,7 @@ public static void ThrowIfZero(T value, [CallerArgumentExpression(nameof(valu { if (value.Equals(default)) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } /// Throws an if is zero. @@ -23,12 +23,12 @@ public static void ThrowIfZero(nint value, [CallerArgumentExpression(nameof(valu { if (value == 0) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } [DoesNotReturn] - static void ThrowZero(string? paramName) => - throw new ArgumentOutOfRangeException(paramName, "Value must not be zero."); + static void ThrowZero(T value, string? paramName) => + throw new ArgumentOutOfRangeException(paramName, value, $"{paramName} ('{value}') must be a non-zero value."); public static void ThrowIfNegative(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : struct, IComparable { @@ -112,7 +112,7 @@ public static void ThrowIfGreaterThanOrEqual(nint value, nint other, [CallerArgu { if (value >= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than {other}."); } } public static void ThrowIfLessThan(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) @@ -144,7 +144,7 @@ public static void ThrowIfLessThanOrEqual(nint value, nint other, [CallerArgumen { if (value <= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than {other}."); } } } diff --git a/src/Split/net481/ConvertPolyfill.cs b/src/Split/net481/ConvertPolyfill.cs index 412674710..1f45391f9 100644 --- a/src/Split/net481/ConvertPolyfill.cs +++ b/src/Split/net481/ConvertPolyfill.cs @@ -134,19 +134,19 @@ public static OperationStatus FromHexString(ReadOnlySpan source, Span= destination.Length) { charsConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal(source[i]); + var lo = GetHexVal(source[i + 1]); + if (hi < 0 || lo < 0) { - charsConsumed = i; + charsConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; @@ -193,19 +193,19 @@ public static OperationStatus FromHexString(ReadOnlySpan utf8Source, Span< var j = 0; while (i + 1 < utf8Source.Length) { - var hi = GetHexVal((char)utf8Source[i]); - var lo = GetHexVal((char)utf8Source[i + 1]); - if (hi < 0 || lo < 0) + if (j >= destination.Length) { bytesConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal((char)utf8Source[i]); + var lo = GetHexVal((char)utf8Source[i + 1]); + if (hi < 0 || lo < 0) { - bytesConsumed = i; + bytesConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; diff --git a/src/Split/net481/DateTimeOffsetPolyfill.cs b/src/Split/net481/DateTimeOffsetPolyfill.cs index 6837790e5..012b944dc 100644 --- a/src/Split/net481/DateTimeOffsetPolyfill.cs +++ b/src/Split/net481/DateTimeOffsetPolyfill.cs @@ -17,7 +17,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTimeOffset(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Offset); diff --git a/src/Split/net481/DateTimePolyfill.cs b/src/Split/net481/DateTimePolyfill.cs index 4641f3e59..430683e14 100644 --- a/src/Split/net481/DateTimePolyfill.cs +++ b/src/Split/net481/DateTimePolyfill.cs @@ -16,7 +16,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTime(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Kind); diff --git a/src/Split/net481/EqualityComparerPolyfill.cs b/src/Split/net481/EqualityComparerPolyfill.cs index e45f2a30e..55cb3ad44 100644 --- a/src/Split/net481/EqualityComparerPolyfill.cs +++ b/src/Split/net481/EqualityComparerPolyfill.cs @@ -19,8 +19,14 @@ public override int GetHashCode(T obj) => /// public static EqualityComparer Create( Func equals, - Func? getHashCode = null) => - new DelegateEqualityComparer(equals, getHashCode); + Func? getHashCode = null) + { + if (equals is null) + { + throw new ArgumentNullException(nameof(equals)); + } + return new DelegateEqualityComparer(equals, getHashCode); + } } class KeySelectorEqualityComparer(Func keySelector, IEqualityComparer? keyComparer) : EqualityComparer @@ -41,7 +47,13 @@ public override int GetHashCode(T obj) /// public static EqualityComparer Create( Func keySelector, - IEqualityComparer? keyComparer = null) => - new KeySelectorEqualityComparer(keySelector, keyComparer); + IEqualityComparer? keyComparer = null) + { + if (keySelector is null) + { + throw new ArgumentNullException(nameof(keySelector)); + } + return new KeySelectorEqualityComparer(keySelector, keyComparer); + } } } diff --git a/src/Split/net481/FileUnixModePolyfill.cs b/src/Split/net481/FileUnixModePolyfill.cs index 2440f7800..701ef07cb 100644 --- a/src/Split/net481/FileUnixModePolyfill.cs +++ b/src/Split/net481/FileUnixModePolyfill.cs @@ -47,8 +47,8 @@ public static UnixFileMode GetUnixFileMode(string path) var mode = output[0] switch { '1' => UnixFileMode.StickyBit, - '2' => UnixFileMode.SetUser, - '4' => UnixFileMode.SetGroup, + '2' => UnixFileMode.SetGroup, + '4' => UnixFileMode.SetUser, _ => UnixFileMode.None }; mode = output[1] switch @@ -158,13 +158,13 @@ public static void SetUnixFileMode(string path, UnixFileMode mode) octal[3] += 1; break; case "setuser": - octal[0] += 2; + octal[0] += 4; break; case "stickybit": octal[0] += 1; break; case "setgroup": - octal[0] += 4; + octal[0] += 2; break; default: throw new("Invalid notation detected"); diff --git a/src/Split/net481/Numbers/DoublePolyfill.cs b/src/Split/net481/Numbers/DoublePolyfill.cs index ba5333f69..2e3e40efd 100644 --- a/src/Split/net481/Numbers/DoublePolyfill.cs +++ b/src/Split/net481/Numbers/DoublePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out double result) => - double.TryParse(s, NumberStyles.Float, provider, out result); + double.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,7 +28,7 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its double-precision floating-point number equivalent.. /// public static bool TryParse(ReadOnlySpan utf8Text, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Converts the span representation of a number in a specified style and culture-specific format to its double-precision floating-point number equivalent. A return value indicates whether the conversion succeeded or failed. /// @@ -43,7 +43,7 @@ public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatPro /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out double result) => - double.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + double.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/net481/Numbers/SinglePolyfill.cs b/src/Split/net481/Numbers/SinglePolyfill.cs index 69af78095..2f6e02a82 100644 --- a/src/Split/net481/Numbers/SinglePolyfill.cs +++ b/src/Split/net481/Numbers/SinglePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out float result) => - float.TryParse(s, NumberStyles.Float, provider, out result); + float.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,7 +28,7 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its single-precision floating-point number equivalent. /// public static bool TryParse(ReadOnlySpan utf8Text, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Converts the span representation of a number in a specified style and culture-specific format to its single-precision floating-point number equivalent. A return value indicates whether the conversion succeeded or failed. /// @@ -43,7 +43,7 @@ public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatPro /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out float result) => - float.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + float.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/net481/Polyfill.cs b/src/Split/net481/Polyfill.cs index 16e916262..b7b21af17 100644 --- a/src/Split/net481/Polyfill.cs +++ b/src/Split/net481/Polyfill.cs @@ -17,5 +17,5 @@ namespace Polyfills; #endif static partial class Polyfill { - const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond * 1000; + const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; } diff --git a/src/Split/net481/Polyfill_ArraySegment.cs b/src/Split/net481/Polyfill_ArraySegment.cs index 96e83c18d..05ea1f659 100644 --- a/src/Split/net481/Polyfill_ArraySegment.cs +++ b/src/Split/net481/Polyfill_ArraySegment.cs @@ -12,15 +12,19 @@ static partial class Polyfill /// public static void CopyTo(this ArraySegment target, ArraySegment destination) { - if (target.Count > destination.Count) + if (target.Array == null) { - throw new ArgumentException("DestinationTooShort", nameof(destination)); + throw new InvalidOperationException("InvalidOperation_NullArray"); } - if (target.Array == null) + if (destination.Array == null) { throw new InvalidOperationException("InvalidOperation_NullArray"); } - Array.Copy(target.Array!, target.Offset, destination.Array, destination.Offset, target.Count); + if (target.Count > destination.Count) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } + Array.Copy(target.Array, target.Offset, destination.Array, destination.Offset, target.Count); } /// /// Copies the contents of this instance into the specified destination array of the same type T. diff --git a/src/Split/net481/Polyfill_Encoding_GetChars.cs b/src/Split/net481/Polyfill_Encoding_GetChars.cs index f5a7de80b..4b956e845 100644 --- a/src/Split/net481/Polyfill_Encoding_GetChars.cs +++ b/src/Split/net481/Polyfill_Encoding_GetChars.cs @@ -25,7 +25,7 @@ public static int GetChars(this Encoding target, ReadOnlySpan bytes, Span< var charArray = new char[bytes.Length]; var array = bytes.ToArray(); var count = target.GetChars(array, 0, bytes.Length, charArray, 0); - new ReadOnlySpan(charArray).CopyTo(chars); + new ReadOnlySpan(charArray, 0, count).CopyTo(chars); return count; } #endif diff --git a/src/Split/net481/Polyfill_List.cs b/src/Split/net481/Polyfill_List.cs index c2e76ed77..7a9348d0c 100644 --- a/src/Split/net481/Polyfill_List.cs +++ b/src/Split/net481/Polyfill_List.cs @@ -21,6 +21,10 @@ public static void AddRange(this List target, ReadOnlySpan source) /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { + if ((uint)index > (uint)target.Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } for (var i = 0; i < source.Length; i++) { var item = source[i]; @@ -30,6 +34,10 @@ public static void InsertRange(this List target, int index, ReadOnlySpanCopies the entire to a span. public static void CopyTo(this List target, Span destination) { + if (target.Count > destination.Length) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } for (var index = 0; index < target.Count; index++) { destination[index] = target[index]; diff --git a/src/Split/net481/Polyfill_Memory_SpanSort.cs b/src/Split/net481/Polyfill_Memory_SpanSort.cs index acc5c8666..b30957972 100644 --- a/src/Split/net481/Polyfill_Memory_SpanSort.cs +++ b/src/Split/net481/Polyfill_Memory_SpanSort.cs @@ -77,12 +77,8 @@ internal ComparerWrapper(Comparison comparison) throw new ArgumentNullException(nameof(comparison)); this.comparison = comparison; } - public int Compare(T? x, T? y) - { - if (x is null || y is null) - return 0; - return comparison((T)x, (T)y); - } + public int Compare(T? x, T? y) => + comparison(x!, y!); } } #endif diff --git a/src/Split/net481/Polyfill_MicroNanosecondAdd.cs b/src/Split/net481/Polyfill_MicroNanosecondAdd.cs index 17d2c8d00..e099e1260 100644 --- a/src/Split/net481/Polyfill_MicroNanosecondAdd.cs +++ b/src/Split/net481/Polyfill_MicroNanosecondAdd.cs @@ -8,10 +8,10 @@ static partial class Polyfill /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTime AddMicroseconds(this DateTime target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); /// /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTimeOffset AddMicroseconds(this DateTimeOffset target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); } diff --git a/src/Split/net481/Polyfill_String.cs b/src/Split/net481/Polyfill_String.cs index bab55956e..254f4be0a 100644 --- a/src/Split/net481/Polyfill_String.cs +++ b/src/Split/net481/Polyfill_String.cs @@ -3,8 +3,6 @@ namespace Polyfills; using System; using System.Text; -using System.IO; -using System.Runtime.CompilerServices; static partial class Polyfill { #if FeatureMemory @@ -147,22 +145,49 @@ public static int LastIndexOf(this string target, char value, int startIndex, in /// /// Replaces all newline sequences in the current string with . /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ReplaceLineEndings(this string target, string replacementText) { + var index = IndexOfNewlineChar(target, 0, out var stride); + if (index < 0) + { + return target; + } var builder = new StringBuilder(target.Length); - using var reader = new StringReader(target); + var position = 0; while (true) { - var line = reader.ReadLine(); - if (line == null) + builder.Append(target, position, index - position); + builder.Append(replacementText); + position = index + stride; + index = IndexOfNewlineChar(target, position, out stride); + if (index < 0) { break; } - builder.Append(line); - builder.Append(replacementText); } - return builder.ToString(0, builder.Length - replacementText.Length); + builder.Append(target, position, target.Length - position); + return builder.ToString(); + } + static int IndexOfNewlineChar(string text, int startIndex, out int stride) + { + stride = 0; + for (var index = startIndex; index < text.Length; index++) + { + switch (text[index]) + { + case '\r': + stride = index + 1 < text.Length && text[index + 1] == '\n' ? 2 : 1; + return index; + case '\n': + case '\f': + case '\u0085': + case '\u2028': + case '\u2029': + stride = 1; + return index; + } + } + return -1; } /// /// Replaces all newline sequences in the current string with . diff --git a/src/Split/net481/Polyfill_StringBuilder_Append.cs b/src/Split/net481/Polyfill_StringBuilder_Append.cs index 0b60749a9..d3970ce53 100644 --- a/src/Split/net481/Polyfill_StringBuilder_Append.cs +++ b/src/Split/net481/Polyfill_StringBuilder_Append.cs @@ -10,6 +10,14 @@ static partial class Polyfill /// public static StringBuilder Append(this StringBuilder target, StringBuilder? value, int startIndex, int count) { + if (startIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(startIndex)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } if (value == null) { if (startIndex == 0 && count == 0) diff --git a/src/Split/net481/Polyfill_StringBuilder_CopyTo.cs b/src/Split/net481/Polyfill_StringBuilder_CopyTo.cs index dc625a7bc..ba99e0a1f 100644 --- a/src/Split/net481/Polyfill_StringBuilder_CopyTo.cs +++ b/src/Split/net481/Polyfill_StringBuilder_CopyTo.cs @@ -15,20 +15,25 @@ public static void CopyTo( Span destination, int count) { - var destinationIndex = 0; - while (true) + if (count < 0) { - if (sourceIndex == target.Length) - { - break; - } - if (destinationIndex == count) - { - break; - } - destination[destinationIndex] = target[sourceIndex]; - destinationIndex++; - sourceIndex++; + throw new ArgumentOutOfRangeException(nameof(count)); + } + if ((uint)sourceIndex > (uint)target.Length) + { + throw new ArgumentOutOfRangeException(nameof(sourceIndex)); + } + if (sourceIndex > target.Length - count) + { + throw new ArgumentException("The source StringBuilder does not contain enough characters from sourceIndex onward to satisfy count."); + } + if (count > destination.Length) + { + throw new ArgumentException("The destination span is too short to hold the requested characters.", nameof(destination)); + } + for (var index = 0; index < count; index++) + { + destination[index] = target[sourceIndex + index]; } } } diff --git a/src/Split/net481/Polyfill_Type.cs b/src/Split/net481/Polyfill_Type.cs index 7890e2168..b99bdfab5 100644 --- a/src/Split/net481/Polyfill_Type.cs +++ b/src/Split/net481/Polyfill_Type.cs @@ -7,9 +7,15 @@ namespace Polyfills; using System.Linq; static partial class Polyfill { - public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInfo other) => - target.MetadataToken == other.MetadataToken && - target.Module.Equals(other.Module); + public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInfo other) + { + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } + return target.MetadataToken == other.MetadataToken && + target.Module.Equals(other.Module); + } /// /// Searches for the specified method whose parameters match the specified generic parameter count, argument types and modifiers, using the specified binding constraints. /// @@ -22,6 +28,25 @@ public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInf Type[] types, ParameterModifier[]? modifiers) { + if (name is null) + { + throw new ArgumentNullException(nameof(name)); + } + if (genericParameterCount < 0) + { + throw new ArgumentOutOfRangeException(nameof(genericParameterCount)); + } + if (types is null) + { + throw new ArgumentNullException(nameof(types)); + } + foreach (var type in types) + { + if (type is null) + { + throw new ArgumentNullException(nameof(types)); + } + } var methods = target.GetMethods(bindingAttr); if (genericParameterCount == 0) { diff --git a/src/Split/net481/RandomNumberGeneratorPolyfill.cs b/src/Split/net481/RandomNumberGeneratorPolyfill.cs index 08384a7ea..539aa7aba 100644 --- a/src/Split/net481/RandomNumberGeneratorPolyfill.cs +++ b/src/Split/net481/RandomNumberGeneratorPolyfill.cs @@ -34,7 +34,7 @@ public static int GetInt32(int fromInclusive, int toExclusive) { generator.GetBytes(bytes); value = BitConverter.ToUInt32(bytes, 0) & mask; - } while (value >= range); + } while (value > range); return (int) (fromInclusive + value); } /// diff --git a/src/Split/net481/StringPolyfill.cs b/src/Split/net481/StringPolyfill.cs index bbdee2733..b14512f1d 100644 --- a/src/Split/net481/StringPolyfill.cs +++ b/src/Split/net481/StringPolyfill.cs @@ -50,6 +50,10 @@ public static string Join(char separator, scoped ReadOnlySpan values) span = span.Slice(1); } var value = values[index]; + if (value is null) + { + continue; + } value.CopyTo(span); span = span.Slice(value.Length); } diff --git a/src/Split/net481/TaskCompletionSource.cs b/src/Split/net481/TaskCompletionSource.cs index 04b507f76..67a35f938 100644 --- a/src/Split/net481/TaskCompletionSource.cs +++ b/src/Split/net481/TaskCompletionSource.cs @@ -77,5 +77,5 @@ public TaskCompletionSource(object? state, TaskCreationOptions creationOptions) /// /// Attempts to transition the underlying into the state. /// - public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(default); + public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(cancellationToken); } diff --git a/src/Split/net481/TimeSpanPolyfill.cs b/src/Split/net481/TimeSpanPolyfill.cs index 94d13cbbb..59d7c7974 100644 --- a/src/Split/net481/TimeSpanPolyfill.cs +++ b/src/Split/net481/TimeSpanPolyfill.cs @@ -15,7 +15,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microseconds => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new TimeSpan(target.Days, target.Hours, target.Minutes, 0); @@ -24,52 +24,71 @@ long TicksComponent() } } const long MicrosecondsToTicks = TimeSpan.TicksPerMillisecond / 1000; + const long MicrosecondsPerMillisecond = 1_000; + const long MicrosecondsPerSecond = 1_000_000; + const long MicrosecondsPerMinute = 60_000_000; + const long MicrosecondsPerHour = 3_600_000_000; + const long MicrosecondsPerDay = 86_400_000_000; + static TimeSpan TicksFromMicroseconds(decimal totalMicroseconds) + { + var ticks = totalMicroseconds * MicrosecondsToTicks; + if (ticks < long.MinValue || ticks > long.MaxValue) + { + throw new ArgumentOutOfRangeException(null, "TimeSpan overflowed because the duration is too long."); + } + return new((long)ticks); + } extension(TimeSpan) { /// /// Initializes a new instance of the structure to a specified number of days, hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromDays(int days, int hours = 0, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)days * TimeSpan.TicksPerDay + - (long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)days * MicrosecondsPerDay + + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromHours(int hours, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromMinutes(int minutes, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of seconds, milliseconds, and microseconds. /// public static TimeSpan FromSeconds(long seconds, long milliseconds = 0, long microseconds = 0) => - new(seconds * TimeSpan.TicksPerSecond + - milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of milliseconds and microseconds. /// public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0) => - new(milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of microseconds. /// public static TimeSpan FromMicroseconds(long microseconds) => - new(microseconds * MicrosecondsToTicks); + TicksFromMicroseconds(microseconds); } } diff --git a/src/Split/net481/XDocumentPolyfill.cs b/src/Split/net481/XDocumentPolyfill.cs index 1611acf19..876842223 100644 --- a/src/Split/net481/XDocumentPolyfill.cs +++ b/src/Split/net481/XDocumentPolyfill.cs @@ -25,12 +25,10 @@ static partial class XDocumentPolyfill /// /// Asynchronously creates a new XDocument and initializes its underlying XML tree using the specified stream, optionally preserving white space. /// - public static async Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) + public static Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - using var reader = new StreamReader(stream); - var content = await reader.ReadToEndAsync(); - return XDocument.Parse(content, options); + return Task.FromResult(XDocument.Load(stream, options)); } /// /// Asynchronously creates a new XDocument and initializes its underlying XML tree using the specified text reader, optionally preserving white space. diff --git a/src/Split/net481/XElementPolyfill.cs b/src/Split/net481/XElementPolyfill.cs index 27208879a..9347d9cdd 100644 --- a/src/Split/net481/XElementPolyfill.cs +++ b/src/Split/net481/XElementPolyfill.cs @@ -25,12 +25,10 @@ static partial class XElementPolyfill /// /// Asynchronously creates a new XElement and initializes its underlying XML tree using the specified stream, optionally preserving white space. /// - public static async Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) + public static Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - using var reader = new StreamReader(stream); - var content = await reader.ReadToEndAsync(); - return XElement.Parse(content, options); + return Task.FromResult(XElement.Load(stream, options)); } /// /// Asynchronously creates a new XElement and initializes its underlying XML tree using the specified text reader, optionally preserving white space. diff --git a/src/Split/net481/ZipFilePolyfill.cs b/src/Split/net481/ZipFilePolyfill.cs index 6cb361e6f..d908c5633 100644 --- a/src/Split/net481/ZipFilePolyfill.cs +++ b/src/Split/net481/ZipFilePolyfill.cs @@ -53,11 +53,12 @@ static void ExtractToDirectoryPolyfill( string destinationDirectory, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + return; } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, null); } static void ExtractToDirectoryPolyfill( string sourceArchiveFile, @@ -65,11 +66,43 @@ static void ExtractToDirectoryPolyfill( Encoding encoding, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); + return; + } + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, encoding); + } + static void ExtractToDirectoryWithOverwrite( + string sourceArchiveFile, + string destinationDirectory, + Encoding? encoding) + { + var destinationRoot = Path.GetFullPath(destinationDirectory); + Directory.CreateDirectory(destinationRoot); + var normalizedRoot = destinationRoot; + if (normalizedRoot[normalizedRoot.Length - 1] != Path.DirectorySeparatorChar) + { + normalizedRoot += Path.DirectorySeparatorChar; + } + using var archive = encoding == null + ? ZipFile.OpenRead(sourceArchiveFile) + : ZipFile.Open(sourceArchiveFile, ZipArchiveMode.Read, encoding); + foreach (var entry in archive.Entries) + { + var destinationPath = Path.GetFullPath(Path.Combine(destinationRoot, entry.FullName)); + if (!destinationPath.StartsWith(normalizedRoot, StringComparison.Ordinal)) + { + throw new IOException("Extracting Zip entry would have resulted in a file outside the specified destination directory."); + } + if (entry.Name.Length == 0) + { + Directory.CreateDirectory(destinationPath); + continue; + } + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + entry.ExtractToFile(destinationPath, overwrite: true); } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); } /// /// Asynchronously opens a ZipArchive on the specified archiveFileName in the specified ZipArchiveMode mode. diff --git a/src/Split/net5.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs b/src/Split/net5.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs index 32b0afded..8bad1d83d 100644 --- a/src/Split/net5.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs +++ b/src/Split/net5.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs @@ -15,7 +15,7 @@ public static void ThrowIfZero(T value, [CallerArgumentExpression(nameof(valu { if (value.Equals(default)) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } /// Throws an if is zero. @@ -23,12 +23,12 @@ public static void ThrowIfZero(nint value, [CallerArgumentExpression(nameof(valu { if (value == 0) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } [DoesNotReturn] - static void ThrowZero(string? paramName) => - throw new ArgumentOutOfRangeException(paramName, "Value must not be zero."); + static void ThrowZero(T value, string? paramName) => + throw new ArgumentOutOfRangeException(paramName, value, $"{paramName} ('{value}') must be a non-zero value."); public static void ThrowIfNegative(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : struct, IComparable { @@ -112,7 +112,7 @@ public static void ThrowIfGreaterThanOrEqual(nint value, nint other, [CallerArgu { if (value >= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than {other}."); } } public static void ThrowIfLessThan(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) @@ -144,7 +144,7 @@ public static void ThrowIfLessThanOrEqual(nint value, nint other, [CallerArgumen { if (value <= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than {other}."); } } } diff --git a/src/Split/net5.0/ConvertPolyfill.cs b/src/Split/net5.0/ConvertPolyfill.cs index a839f8dae..34e795be6 100644 --- a/src/Split/net5.0/ConvertPolyfill.cs +++ b/src/Split/net5.0/ConvertPolyfill.cs @@ -30,19 +30,19 @@ public static OperationStatus FromHexString(ReadOnlySpan source, Span= destination.Length) { charsConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal(source[i]); + var lo = GetHexVal(source[i + 1]); + if (hi < 0 || lo < 0) { - charsConsumed = i; + charsConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; @@ -89,19 +89,19 @@ public static OperationStatus FromHexString(ReadOnlySpan utf8Source, Span< var j = 0; while (i + 1 < utf8Source.Length) { - var hi = GetHexVal((char)utf8Source[i]); - var lo = GetHexVal((char)utf8Source[i + 1]); - if (hi < 0 || lo < 0) + if (j >= destination.Length) { bytesConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal((char)utf8Source[i]); + var lo = GetHexVal((char)utf8Source[i + 1]); + if (hi < 0 || lo < 0) { - bytesConsumed = i; + bytesConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; diff --git a/src/Split/net5.0/DateTimeOffsetPolyfill.cs b/src/Split/net5.0/DateTimeOffsetPolyfill.cs index 15ba54f72..f0cf6a209 100644 --- a/src/Split/net5.0/DateTimeOffsetPolyfill.cs +++ b/src/Split/net5.0/DateTimeOffsetPolyfill.cs @@ -17,7 +17,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTimeOffset(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Offset); diff --git a/src/Split/net5.0/DateTimePolyfill.cs b/src/Split/net5.0/DateTimePolyfill.cs index 8eb8c705b..faa990de5 100644 --- a/src/Split/net5.0/DateTimePolyfill.cs +++ b/src/Split/net5.0/DateTimePolyfill.cs @@ -16,7 +16,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTime(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Kind); diff --git a/src/Split/net5.0/EqualityComparerPolyfill.cs b/src/Split/net5.0/EqualityComparerPolyfill.cs index e45f2a30e..55cb3ad44 100644 --- a/src/Split/net5.0/EqualityComparerPolyfill.cs +++ b/src/Split/net5.0/EqualityComparerPolyfill.cs @@ -19,8 +19,14 @@ public override int GetHashCode(T obj) => /// public static EqualityComparer Create( Func equals, - Func? getHashCode = null) => - new DelegateEqualityComparer(equals, getHashCode); + Func? getHashCode = null) + { + if (equals is null) + { + throw new ArgumentNullException(nameof(equals)); + } + return new DelegateEqualityComparer(equals, getHashCode); + } } class KeySelectorEqualityComparer(Func keySelector, IEqualityComparer? keyComparer) : EqualityComparer @@ -41,7 +47,13 @@ public override int GetHashCode(T obj) /// public static EqualityComparer Create( Func keySelector, - IEqualityComparer? keyComparer = null) => - new KeySelectorEqualityComparer(keySelector, keyComparer); + IEqualityComparer? keyComparer = null) + { + if (keySelector is null) + { + throw new ArgumentNullException(nameof(keySelector)); + } + return new KeySelectorEqualityComparer(keySelector, keyComparer); + } } } diff --git a/src/Split/net5.0/FileUnixModePolyfill.cs b/src/Split/net5.0/FileUnixModePolyfill.cs index 2440f7800..701ef07cb 100644 --- a/src/Split/net5.0/FileUnixModePolyfill.cs +++ b/src/Split/net5.0/FileUnixModePolyfill.cs @@ -47,8 +47,8 @@ public static UnixFileMode GetUnixFileMode(string path) var mode = output[0] switch { '1' => UnixFileMode.StickyBit, - '2' => UnixFileMode.SetUser, - '4' => UnixFileMode.SetGroup, + '2' => UnixFileMode.SetGroup, + '4' => UnixFileMode.SetUser, _ => UnixFileMode.None }; mode = output[1] switch @@ -158,13 +158,13 @@ public static void SetUnixFileMode(string path, UnixFileMode mode) octal[3] += 1; break; case "setuser": - octal[0] += 2; + octal[0] += 4; break; case "stickybit": octal[0] += 1; break; case "setgroup": - octal[0] += 4; + octal[0] += 2; break; default: throw new("Invalid notation detected"); diff --git a/src/Split/net5.0/Numbers/DoublePolyfill.cs b/src/Split/net5.0/Numbers/DoublePolyfill.cs index ff9f8aca6..d08a8d3b6 100644 --- a/src/Split/net5.0/Numbers/DoublePolyfill.cs +++ b/src/Split/net5.0/Numbers/DoublePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out double result) => - double.TryParse(s, NumberStyles.Float, provider, out result); + double.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,12 +28,12 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its double-precision floating-point number equivalent.. /// public static bool TryParse(ReadOnlySpan utf8Text, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out double result) => - double.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + double.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/net5.0/Numbers/SinglePolyfill.cs b/src/Split/net5.0/Numbers/SinglePolyfill.cs index 3d574e6c7..6a49cbf6a 100644 --- a/src/Split/net5.0/Numbers/SinglePolyfill.cs +++ b/src/Split/net5.0/Numbers/SinglePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out float result) => - float.TryParse(s, NumberStyles.Float, provider, out result); + float.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,12 +28,12 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its single-precision floating-point number equivalent. /// public static bool TryParse(ReadOnlySpan utf8Text, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out float result) => - float.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + float.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/net5.0/Polyfill.cs b/src/Split/net5.0/Polyfill.cs index 16e916262..b7b21af17 100644 --- a/src/Split/net5.0/Polyfill.cs +++ b/src/Split/net5.0/Polyfill.cs @@ -17,5 +17,5 @@ namespace Polyfills; #endif static partial class Polyfill { - const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond * 1000; + const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; } diff --git a/src/Split/net5.0/Polyfill_List.cs b/src/Split/net5.0/Polyfill_List.cs index c2e76ed77..7a9348d0c 100644 --- a/src/Split/net5.0/Polyfill_List.cs +++ b/src/Split/net5.0/Polyfill_List.cs @@ -21,6 +21,10 @@ public static void AddRange(this List target, ReadOnlySpan source) /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { + if ((uint)index > (uint)target.Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } for (var i = 0; i < source.Length; i++) { var item = source[i]; @@ -30,6 +34,10 @@ public static void InsertRange(this List target, int index, ReadOnlySpanCopies the entire to a span. public static void CopyTo(this List target, Span destination) { + if (target.Count > destination.Length) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } for (var index = 0; index < target.Count; index++) { destination[index] = target[index]; diff --git a/src/Split/net5.0/Polyfill_MicroNanosecondAdd.cs b/src/Split/net5.0/Polyfill_MicroNanosecondAdd.cs index 17d2c8d00..e099e1260 100644 --- a/src/Split/net5.0/Polyfill_MicroNanosecondAdd.cs +++ b/src/Split/net5.0/Polyfill_MicroNanosecondAdd.cs @@ -8,10 +8,10 @@ static partial class Polyfill /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTime AddMicroseconds(this DateTime target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); /// /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTimeOffset AddMicroseconds(this DateTimeOffset target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); } diff --git a/src/Split/net5.0/Polyfill_String.cs b/src/Split/net5.0/Polyfill_String.cs index 2f9a508fd..bb7f8cfd4 100644 --- a/src/Split/net5.0/Polyfill_String.cs +++ b/src/Split/net5.0/Polyfill_String.cs @@ -3,8 +3,6 @@ namespace Polyfills; using System; using System.Text; -using System.IO; -using System.Runtime.CompilerServices; static partial class Polyfill { #if FeatureMemory @@ -66,22 +64,49 @@ public static int LastIndexOf(this string target, char value, int startIndex, in /// /// Replaces all newline sequences in the current string with . /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ReplaceLineEndings(this string target, string replacementText) { + var index = IndexOfNewlineChar(target, 0, out var stride); + if (index < 0) + { + return target; + } var builder = new StringBuilder(target.Length); - using var reader = new StringReader(target); + var position = 0; while (true) { - var line = reader.ReadLine(); - if (line == null) + builder.Append(target, position, index - position); + builder.Append(replacementText); + position = index + stride; + index = IndexOfNewlineChar(target, position, out stride); + if (index < 0) { break; } - builder.Append(line); - builder.Append(replacementText); } - return builder.ToString(0, builder.Length - replacementText.Length); + builder.Append(target, position, target.Length - position); + return builder.ToString(); + } + static int IndexOfNewlineChar(string text, int startIndex, out int stride) + { + stride = 0; + for (var index = startIndex; index < text.Length; index++) + { + switch (text[index]) + { + case '\r': + stride = index + 1 < text.Length && text[index + 1] == '\n' ? 2 : 1; + return index; + case '\n': + case '\f': + case '\u0085': + case '\u2028': + case '\u2029': + stride = 1; + return index; + } + } + return -1; } /// /// Replaces all newline sequences in the current string with . diff --git a/src/Split/net5.0/StringPolyfill.cs b/src/Split/net5.0/StringPolyfill.cs index 747f914bb..7d39ca017 100644 --- a/src/Split/net5.0/StringPolyfill.cs +++ b/src/Split/net5.0/StringPolyfill.cs @@ -50,6 +50,10 @@ public static string Join(char separator, scoped ReadOnlySpan values) span = span.Slice(1); } var value = values[index]; + if (value is null) + { + continue; + } value.CopyTo(span); span = span.Slice(value.Length); } diff --git a/src/Split/net5.0/TimeSpanPolyfill.cs b/src/Split/net5.0/TimeSpanPolyfill.cs index 94d13cbbb..59d7c7974 100644 --- a/src/Split/net5.0/TimeSpanPolyfill.cs +++ b/src/Split/net5.0/TimeSpanPolyfill.cs @@ -15,7 +15,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microseconds => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new TimeSpan(target.Days, target.Hours, target.Minutes, 0); @@ -24,52 +24,71 @@ long TicksComponent() } } const long MicrosecondsToTicks = TimeSpan.TicksPerMillisecond / 1000; + const long MicrosecondsPerMillisecond = 1_000; + const long MicrosecondsPerSecond = 1_000_000; + const long MicrosecondsPerMinute = 60_000_000; + const long MicrosecondsPerHour = 3_600_000_000; + const long MicrosecondsPerDay = 86_400_000_000; + static TimeSpan TicksFromMicroseconds(decimal totalMicroseconds) + { + var ticks = totalMicroseconds * MicrosecondsToTicks; + if (ticks < long.MinValue || ticks > long.MaxValue) + { + throw new ArgumentOutOfRangeException(null, "TimeSpan overflowed because the duration is too long."); + } + return new((long)ticks); + } extension(TimeSpan) { /// /// Initializes a new instance of the structure to a specified number of days, hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromDays(int days, int hours = 0, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)days * TimeSpan.TicksPerDay + - (long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)days * MicrosecondsPerDay + + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromHours(int hours, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromMinutes(int minutes, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of seconds, milliseconds, and microseconds. /// public static TimeSpan FromSeconds(long seconds, long milliseconds = 0, long microseconds = 0) => - new(seconds * TimeSpan.TicksPerSecond + - milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of milliseconds and microseconds. /// public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0) => - new(milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of microseconds. /// public static TimeSpan FromMicroseconds(long microseconds) => - new(microseconds * MicrosecondsToTicks); + TicksFromMicroseconds(microseconds); } } diff --git a/src/Split/net5.0/ZipFilePolyfill.cs b/src/Split/net5.0/ZipFilePolyfill.cs index 6cb361e6f..d908c5633 100644 --- a/src/Split/net5.0/ZipFilePolyfill.cs +++ b/src/Split/net5.0/ZipFilePolyfill.cs @@ -53,11 +53,12 @@ static void ExtractToDirectoryPolyfill( string destinationDirectory, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + return; } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, null); } static void ExtractToDirectoryPolyfill( string sourceArchiveFile, @@ -65,11 +66,43 @@ static void ExtractToDirectoryPolyfill( Encoding encoding, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); + return; + } + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, encoding); + } + static void ExtractToDirectoryWithOverwrite( + string sourceArchiveFile, + string destinationDirectory, + Encoding? encoding) + { + var destinationRoot = Path.GetFullPath(destinationDirectory); + Directory.CreateDirectory(destinationRoot); + var normalizedRoot = destinationRoot; + if (normalizedRoot[normalizedRoot.Length - 1] != Path.DirectorySeparatorChar) + { + normalizedRoot += Path.DirectorySeparatorChar; + } + using var archive = encoding == null + ? ZipFile.OpenRead(sourceArchiveFile) + : ZipFile.Open(sourceArchiveFile, ZipArchiveMode.Read, encoding); + foreach (var entry in archive.Entries) + { + var destinationPath = Path.GetFullPath(Path.Combine(destinationRoot, entry.FullName)); + if (!destinationPath.StartsWith(normalizedRoot, StringComparison.Ordinal)) + { + throw new IOException("Extracting Zip entry would have resulted in a file outside the specified destination directory."); + } + if (entry.Name.Length == 0) + { + Directory.CreateDirectory(destinationPath); + continue; + } + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + entry.ExtractToFile(destinationPath, overwrite: true); } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); } /// /// Asynchronously opens a ZipArchive on the specified archiveFileName in the specified ZipArchiveMode mode. diff --git a/src/Split/net6.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs b/src/Split/net6.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs index 32b0afded..8bad1d83d 100644 --- a/src/Split/net6.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs +++ b/src/Split/net6.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs @@ -15,7 +15,7 @@ public static void ThrowIfZero(T value, [CallerArgumentExpression(nameof(valu { if (value.Equals(default)) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } /// Throws an if is zero. @@ -23,12 +23,12 @@ public static void ThrowIfZero(nint value, [CallerArgumentExpression(nameof(valu { if (value == 0) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } [DoesNotReturn] - static void ThrowZero(string? paramName) => - throw new ArgumentOutOfRangeException(paramName, "Value must not be zero."); + static void ThrowZero(T value, string? paramName) => + throw new ArgumentOutOfRangeException(paramName, value, $"{paramName} ('{value}') must be a non-zero value."); public static void ThrowIfNegative(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : struct, IComparable { @@ -112,7 +112,7 @@ public static void ThrowIfGreaterThanOrEqual(nint value, nint other, [CallerArgu { if (value >= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than {other}."); } } public static void ThrowIfLessThan(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) @@ -144,7 +144,7 @@ public static void ThrowIfLessThanOrEqual(nint value, nint other, [CallerArgumen { if (value <= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than {other}."); } } } diff --git a/src/Split/net6.0/ConvertPolyfill.cs b/src/Split/net6.0/ConvertPolyfill.cs index a839f8dae..34e795be6 100644 --- a/src/Split/net6.0/ConvertPolyfill.cs +++ b/src/Split/net6.0/ConvertPolyfill.cs @@ -30,19 +30,19 @@ public static OperationStatus FromHexString(ReadOnlySpan source, Span= destination.Length) { charsConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal(source[i]); + var lo = GetHexVal(source[i + 1]); + if (hi < 0 || lo < 0) { - charsConsumed = i; + charsConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; @@ -89,19 +89,19 @@ public static OperationStatus FromHexString(ReadOnlySpan utf8Source, Span< var j = 0; while (i + 1 < utf8Source.Length) { - var hi = GetHexVal((char)utf8Source[i]); - var lo = GetHexVal((char)utf8Source[i + 1]); - if (hi < 0 || lo < 0) + if (j >= destination.Length) { bytesConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal((char)utf8Source[i]); + var lo = GetHexVal((char)utf8Source[i + 1]); + if (hi < 0 || lo < 0) { - bytesConsumed = i; + bytesConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; diff --git a/src/Split/net6.0/DateTimeOffsetPolyfill.cs b/src/Split/net6.0/DateTimeOffsetPolyfill.cs index 15ba54f72..f0cf6a209 100644 --- a/src/Split/net6.0/DateTimeOffsetPolyfill.cs +++ b/src/Split/net6.0/DateTimeOffsetPolyfill.cs @@ -17,7 +17,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTimeOffset(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Offset); diff --git a/src/Split/net6.0/DateTimePolyfill.cs b/src/Split/net6.0/DateTimePolyfill.cs index 8eb8c705b..faa990de5 100644 --- a/src/Split/net6.0/DateTimePolyfill.cs +++ b/src/Split/net6.0/DateTimePolyfill.cs @@ -16,7 +16,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTime(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Kind); diff --git a/src/Split/net6.0/EqualityComparerPolyfill.cs b/src/Split/net6.0/EqualityComparerPolyfill.cs index e45f2a30e..55cb3ad44 100644 --- a/src/Split/net6.0/EqualityComparerPolyfill.cs +++ b/src/Split/net6.0/EqualityComparerPolyfill.cs @@ -19,8 +19,14 @@ public override int GetHashCode(T obj) => /// public static EqualityComparer Create( Func equals, - Func? getHashCode = null) => - new DelegateEqualityComparer(equals, getHashCode); + Func? getHashCode = null) + { + if (equals is null) + { + throw new ArgumentNullException(nameof(equals)); + } + return new DelegateEqualityComparer(equals, getHashCode); + } } class KeySelectorEqualityComparer(Func keySelector, IEqualityComparer? keyComparer) : EqualityComparer @@ -41,7 +47,13 @@ public override int GetHashCode(T obj) /// public static EqualityComparer Create( Func keySelector, - IEqualityComparer? keyComparer = null) => - new KeySelectorEqualityComparer(keySelector, keyComparer); + IEqualityComparer? keyComparer = null) + { + if (keySelector is null) + { + throw new ArgumentNullException(nameof(keySelector)); + } + return new KeySelectorEqualityComparer(keySelector, keyComparer); + } } } diff --git a/src/Split/net6.0/FileUnixModePolyfill.cs b/src/Split/net6.0/FileUnixModePolyfill.cs index 2440f7800..701ef07cb 100644 --- a/src/Split/net6.0/FileUnixModePolyfill.cs +++ b/src/Split/net6.0/FileUnixModePolyfill.cs @@ -47,8 +47,8 @@ public static UnixFileMode GetUnixFileMode(string path) var mode = output[0] switch { '1' => UnixFileMode.StickyBit, - '2' => UnixFileMode.SetUser, - '4' => UnixFileMode.SetGroup, + '2' => UnixFileMode.SetGroup, + '4' => UnixFileMode.SetUser, _ => UnixFileMode.None }; mode = output[1] switch @@ -158,13 +158,13 @@ public static void SetUnixFileMode(string path, UnixFileMode mode) octal[3] += 1; break; case "setuser": - octal[0] += 2; + octal[0] += 4; break; case "stickybit": octal[0] += 1; break; case "setgroup": - octal[0] += 4; + octal[0] += 2; break; default: throw new("Invalid notation detected"); diff --git a/src/Split/net6.0/Numbers/DoublePolyfill.cs b/src/Split/net6.0/Numbers/DoublePolyfill.cs index ff9f8aca6..d08a8d3b6 100644 --- a/src/Split/net6.0/Numbers/DoublePolyfill.cs +++ b/src/Split/net6.0/Numbers/DoublePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out double result) => - double.TryParse(s, NumberStyles.Float, provider, out result); + double.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,12 +28,12 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its double-precision floating-point number equivalent.. /// public static bool TryParse(ReadOnlySpan utf8Text, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out double result) => - double.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + double.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/net6.0/Numbers/SinglePolyfill.cs b/src/Split/net6.0/Numbers/SinglePolyfill.cs index 3d574e6c7..6a49cbf6a 100644 --- a/src/Split/net6.0/Numbers/SinglePolyfill.cs +++ b/src/Split/net6.0/Numbers/SinglePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out float result) => - float.TryParse(s, NumberStyles.Float, provider, out result); + float.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,12 +28,12 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its single-precision floating-point number equivalent. /// public static bool TryParse(ReadOnlySpan utf8Text, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out float result) => - float.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + float.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/net6.0/Polyfill.cs b/src/Split/net6.0/Polyfill.cs index 16e916262..b7b21af17 100644 --- a/src/Split/net6.0/Polyfill.cs +++ b/src/Split/net6.0/Polyfill.cs @@ -17,5 +17,5 @@ namespace Polyfills; #endif static partial class Polyfill { - const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond * 1000; + const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; } diff --git a/src/Split/net6.0/Polyfill_List.cs b/src/Split/net6.0/Polyfill_List.cs index c008098ab..266ba2939 100644 --- a/src/Split/net6.0/Polyfill_List.cs +++ b/src/Split/net6.0/Polyfill_List.cs @@ -21,6 +21,10 @@ public static void AddRange(this List target, ReadOnlySpan source) /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { + if ((uint)index > (uint)target.Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } for (var i = 0; i < source.Length; i++) { var item = source[i]; @@ -30,6 +34,10 @@ public static void InsertRange(this List target, int index, ReadOnlySpanCopies the entire to a span. public static void CopyTo(this List target, Span destination) { + if (target.Count > destination.Length) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } for (var index = 0; index < target.Count; index++) { destination[index] = target[index]; diff --git a/src/Split/net6.0/Polyfill_MicroNanosecondAdd.cs b/src/Split/net6.0/Polyfill_MicroNanosecondAdd.cs index 17d2c8d00..e099e1260 100644 --- a/src/Split/net6.0/Polyfill_MicroNanosecondAdd.cs +++ b/src/Split/net6.0/Polyfill_MicroNanosecondAdd.cs @@ -8,10 +8,10 @@ static partial class Polyfill /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTime AddMicroseconds(this DateTime target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); /// /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTimeOffset AddMicroseconds(this DateTimeOffset target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); } diff --git a/src/Split/net6.0/Polyfill_String.cs b/src/Split/net6.0/Polyfill_String.cs index dc72e38f7..99ec08a0c 100644 --- a/src/Split/net6.0/Polyfill_String.cs +++ b/src/Split/net6.0/Polyfill_String.cs @@ -3,8 +3,6 @@ namespace Polyfills; using System; using System.Text; -using System.IO; -using System.Runtime.CompilerServices; static partial class Polyfill { /// diff --git a/src/Split/net6.0/StringPolyfill.cs b/src/Split/net6.0/StringPolyfill.cs index 747f914bb..7d39ca017 100644 --- a/src/Split/net6.0/StringPolyfill.cs +++ b/src/Split/net6.0/StringPolyfill.cs @@ -50,6 +50,10 @@ public static string Join(char separator, scoped ReadOnlySpan values) span = span.Slice(1); } var value = values[index]; + if (value is null) + { + continue; + } value.CopyTo(span); span = span.Slice(value.Length); } diff --git a/src/Split/net6.0/TimeSpanPolyfill.cs b/src/Split/net6.0/TimeSpanPolyfill.cs index 94d13cbbb..59d7c7974 100644 --- a/src/Split/net6.0/TimeSpanPolyfill.cs +++ b/src/Split/net6.0/TimeSpanPolyfill.cs @@ -15,7 +15,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microseconds => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new TimeSpan(target.Days, target.Hours, target.Minutes, 0); @@ -24,52 +24,71 @@ long TicksComponent() } } const long MicrosecondsToTicks = TimeSpan.TicksPerMillisecond / 1000; + const long MicrosecondsPerMillisecond = 1_000; + const long MicrosecondsPerSecond = 1_000_000; + const long MicrosecondsPerMinute = 60_000_000; + const long MicrosecondsPerHour = 3_600_000_000; + const long MicrosecondsPerDay = 86_400_000_000; + static TimeSpan TicksFromMicroseconds(decimal totalMicroseconds) + { + var ticks = totalMicroseconds * MicrosecondsToTicks; + if (ticks < long.MinValue || ticks > long.MaxValue) + { + throw new ArgumentOutOfRangeException(null, "TimeSpan overflowed because the duration is too long."); + } + return new((long)ticks); + } extension(TimeSpan) { /// /// Initializes a new instance of the structure to a specified number of days, hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromDays(int days, int hours = 0, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)days * TimeSpan.TicksPerDay + - (long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)days * MicrosecondsPerDay + + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromHours(int hours, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromMinutes(int minutes, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of seconds, milliseconds, and microseconds. /// public static TimeSpan FromSeconds(long seconds, long milliseconds = 0, long microseconds = 0) => - new(seconds * TimeSpan.TicksPerSecond + - milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of milliseconds and microseconds. /// public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0) => - new(milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of microseconds. /// public static TimeSpan FromMicroseconds(long microseconds) => - new(microseconds * MicrosecondsToTicks); + TicksFromMicroseconds(microseconds); } } diff --git a/src/Split/net6.0/ZipFilePolyfill.cs b/src/Split/net6.0/ZipFilePolyfill.cs index 6cb361e6f..d908c5633 100644 --- a/src/Split/net6.0/ZipFilePolyfill.cs +++ b/src/Split/net6.0/ZipFilePolyfill.cs @@ -53,11 +53,12 @@ static void ExtractToDirectoryPolyfill( string destinationDirectory, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + return; } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, null); } static void ExtractToDirectoryPolyfill( string sourceArchiveFile, @@ -65,11 +66,43 @@ static void ExtractToDirectoryPolyfill( Encoding encoding, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); + return; + } + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, encoding); + } + static void ExtractToDirectoryWithOverwrite( + string sourceArchiveFile, + string destinationDirectory, + Encoding? encoding) + { + var destinationRoot = Path.GetFullPath(destinationDirectory); + Directory.CreateDirectory(destinationRoot); + var normalizedRoot = destinationRoot; + if (normalizedRoot[normalizedRoot.Length - 1] != Path.DirectorySeparatorChar) + { + normalizedRoot += Path.DirectorySeparatorChar; + } + using var archive = encoding == null + ? ZipFile.OpenRead(sourceArchiveFile) + : ZipFile.Open(sourceArchiveFile, ZipArchiveMode.Read, encoding); + foreach (var entry in archive.Entries) + { + var destinationPath = Path.GetFullPath(Path.Combine(destinationRoot, entry.FullName)); + if (!destinationPath.StartsWith(normalizedRoot, StringComparison.Ordinal)) + { + throw new IOException("Extracting Zip entry would have resulted in a file outside the specified destination directory."); + } + if (entry.Name.Length == 0) + { + Directory.CreateDirectory(destinationPath); + continue; + } + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + entry.ExtractToFile(destinationPath, overwrite: true); } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); } /// /// Asynchronously opens a ZipArchive on the specified archiveFileName in the specified ZipArchiveMode mode. diff --git a/src/Split/net7.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs b/src/Split/net7.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs index e662d216a..dd7087e57 100644 --- a/src/Split/net7.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs +++ b/src/Split/net7.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs @@ -15,12 +15,12 @@ public static void ThrowIfZero(T value, [CallerArgumentExpression(nameof(valu { if (T.IsZero(value)) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } [DoesNotReturn] - static void ThrowZero(string? paramName) => - throw new ArgumentOutOfRangeException(paramName, "Value must not be zero."); + static void ThrowZero(T value, string? paramName) => + throw new ArgumentOutOfRangeException(paramName, value, $"{paramName} ('{value}') must be a non-zero value."); public static void ThrowIfNegative(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : INumberBase { @@ -88,7 +88,7 @@ public static void ThrowIfGreaterThanOrEqual(nint value, nint other, [CallerArgu { if (value >= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than {other}."); } } public static void ThrowIfLessThan(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) @@ -120,7 +120,7 @@ public static void ThrowIfLessThanOrEqual(nint value, nint other, [CallerArgumen { if (value <= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than {other}."); } } } diff --git a/src/Split/net7.0/ConvertPolyfill.cs b/src/Split/net7.0/ConvertPolyfill.cs index a839f8dae..34e795be6 100644 --- a/src/Split/net7.0/ConvertPolyfill.cs +++ b/src/Split/net7.0/ConvertPolyfill.cs @@ -30,19 +30,19 @@ public static OperationStatus FromHexString(ReadOnlySpan source, Span= destination.Length) { charsConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal(source[i]); + var lo = GetHexVal(source[i + 1]); + if (hi < 0 || lo < 0) { - charsConsumed = i; + charsConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; @@ -89,19 +89,19 @@ public static OperationStatus FromHexString(ReadOnlySpan utf8Source, Span< var j = 0; while (i + 1 < utf8Source.Length) { - var hi = GetHexVal((char)utf8Source[i]); - var lo = GetHexVal((char)utf8Source[i + 1]); - if (hi < 0 || lo < 0) + if (j >= destination.Length) { bytesConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal((char)utf8Source[i]); + var lo = GetHexVal((char)utf8Source[i + 1]); + if (hi < 0 || lo < 0) { - bytesConsumed = i; + bytesConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; diff --git a/src/Split/net7.0/EqualityComparerPolyfill.cs b/src/Split/net7.0/EqualityComparerPolyfill.cs index e45f2a30e..55cb3ad44 100644 --- a/src/Split/net7.0/EqualityComparerPolyfill.cs +++ b/src/Split/net7.0/EqualityComparerPolyfill.cs @@ -19,8 +19,14 @@ public override int GetHashCode(T obj) => /// public static EqualityComparer Create( Func equals, - Func? getHashCode = null) => - new DelegateEqualityComparer(equals, getHashCode); + Func? getHashCode = null) + { + if (equals is null) + { + throw new ArgumentNullException(nameof(equals)); + } + return new DelegateEqualityComparer(equals, getHashCode); + } } class KeySelectorEqualityComparer(Func keySelector, IEqualityComparer? keyComparer) : EqualityComparer @@ -41,7 +47,13 @@ public override int GetHashCode(T obj) /// public static EqualityComparer Create( Func keySelector, - IEqualityComparer? keyComparer = null) => - new KeySelectorEqualityComparer(keySelector, keyComparer); + IEqualityComparer? keyComparer = null) + { + if (keySelector is null) + { + throw new ArgumentNullException(nameof(keySelector)); + } + return new KeySelectorEqualityComparer(keySelector, keyComparer); + } } } diff --git a/src/Split/net7.0/Numbers/DoublePolyfill.cs b/src/Split/net7.0/Numbers/DoublePolyfill.cs index 53d86a1a7..7f2557cd5 100644 --- a/src/Split/net7.0/Numbers/DoublePolyfill.cs +++ b/src/Split/net7.0/Numbers/DoublePolyfill.cs @@ -13,7 +13,7 @@ static partial class Polyfill /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -23,7 +23,7 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its double-precision floating-point number equivalent.. /// public static bool TryParse(ReadOnlySpan utf8Text, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); #endif } } diff --git a/src/Split/net7.0/Numbers/SinglePolyfill.cs b/src/Split/net7.0/Numbers/SinglePolyfill.cs index 0cac1be46..e80d3aac9 100644 --- a/src/Split/net7.0/Numbers/SinglePolyfill.cs +++ b/src/Split/net7.0/Numbers/SinglePolyfill.cs @@ -13,7 +13,7 @@ static partial class Polyfill /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -23,7 +23,7 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its single-precision floating-point number equivalent. /// public static bool TryParse(ReadOnlySpan utf8Text, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); #endif } } diff --git a/src/Split/net7.0/Polyfill.cs b/src/Split/net7.0/Polyfill.cs index 16e916262..b7b21af17 100644 --- a/src/Split/net7.0/Polyfill.cs +++ b/src/Split/net7.0/Polyfill.cs @@ -17,5 +17,5 @@ namespace Polyfills; #endif static partial class Polyfill { - const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond * 1000; + const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; } diff --git a/src/Split/net7.0/Polyfill_List.cs b/src/Split/net7.0/Polyfill_List.cs index af5bf9dfe..22d8120e4 100644 --- a/src/Split/net7.0/Polyfill_List.cs +++ b/src/Split/net7.0/Polyfill_List.cs @@ -18,6 +18,10 @@ public static void AddRange(this List target, ReadOnlySpan source) /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { + if ((uint)index > (uint)target.Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } for (var i = 0; i < source.Length; i++) { var item = source[i]; @@ -27,6 +31,10 @@ public static void InsertRange(this List target, int index, ReadOnlySpanCopies the entire to a span. public static void CopyTo(this List target, Span destination) { + if (target.Count > destination.Length) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } for (var index = 0; index < target.Count; index++) { destination[index] = target[index]; diff --git a/src/Split/net7.0/Polyfill_String.cs b/src/Split/net7.0/Polyfill_String.cs index dc72e38f7..99ec08a0c 100644 --- a/src/Split/net7.0/Polyfill_String.cs +++ b/src/Split/net7.0/Polyfill_String.cs @@ -3,8 +3,6 @@ namespace Polyfills; using System; using System.Text; -using System.IO; -using System.Runtime.CompilerServices; static partial class Polyfill { /// diff --git a/src/Split/net7.0/StringPolyfill.cs b/src/Split/net7.0/StringPolyfill.cs index 747f914bb..7d39ca017 100644 --- a/src/Split/net7.0/StringPolyfill.cs +++ b/src/Split/net7.0/StringPolyfill.cs @@ -50,6 +50,10 @@ public static string Join(char separator, scoped ReadOnlySpan values) span = span.Slice(1); } var value = values[index]; + if (value is null) + { + continue; + } value.CopyTo(span); span = span.Slice(value.Length); } diff --git a/src/Split/net7.0/TimeSpanPolyfill.cs b/src/Split/net7.0/TimeSpanPolyfill.cs index 0a4c8afd0..bad6237ed 100644 --- a/src/Split/net7.0/TimeSpanPolyfill.cs +++ b/src/Split/net7.0/TimeSpanPolyfill.cs @@ -5,52 +5,71 @@ namespace Polyfills; static partial class Polyfill { const long MicrosecondsToTicks = TimeSpan.TicksPerMillisecond / 1000; + const long MicrosecondsPerMillisecond = 1_000; + const long MicrosecondsPerSecond = 1_000_000; + const long MicrosecondsPerMinute = 60_000_000; + const long MicrosecondsPerHour = 3_600_000_000; + const long MicrosecondsPerDay = 86_400_000_000; + static TimeSpan TicksFromMicroseconds(decimal totalMicroseconds) + { + var ticks = totalMicroseconds * MicrosecondsToTicks; + if (ticks < long.MinValue || ticks > long.MaxValue) + { + throw new ArgumentOutOfRangeException(null, "TimeSpan overflowed because the duration is too long."); + } + return new((long)ticks); + } extension(TimeSpan) { /// /// Initializes a new instance of the structure to a specified number of days, hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromDays(int days, int hours = 0, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)days * TimeSpan.TicksPerDay + - (long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)days * MicrosecondsPerDay + + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromHours(int hours, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromMinutes(int minutes, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of seconds, milliseconds, and microseconds. /// public static TimeSpan FromSeconds(long seconds, long milliseconds = 0, long microseconds = 0) => - new(seconds * TimeSpan.TicksPerSecond + - milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of milliseconds and microseconds. /// public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0) => - new(milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of microseconds. /// public static TimeSpan FromMicroseconds(long microseconds) => - new(microseconds * MicrosecondsToTicks); + TicksFromMicroseconds(microseconds); } } diff --git a/src/Split/net7.0/ZipFilePolyfill.cs b/src/Split/net7.0/ZipFilePolyfill.cs index 6cb361e6f..d908c5633 100644 --- a/src/Split/net7.0/ZipFilePolyfill.cs +++ b/src/Split/net7.0/ZipFilePolyfill.cs @@ -53,11 +53,12 @@ static void ExtractToDirectoryPolyfill( string destinationDirectory, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + return; } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, null); } static void ExtractToDirectoryPolyfill( string sourceArchiveFile, @@ -65,11 +66,43 @@ static void ExtractToDirectoryPolyfill( Encoding encoding, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); + return; + } + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, encoding); + } + static void ExtractToDirectoryWithOverwrite( + string sourceArchiveFile, + string destinationDirectory, + Encoding? encoding) + { + var destinationRoot = Path.GetFullPath(destinationDirectory); + Directory.CreateDirectory(destinationRoot); + var normalizedRoot = destinationRoot; + if (normalizedRoot[normalizedRoot.Length - 1] != Path.DirectorySeparatorChar) + { + normalizedRoot += Path.DirectorySeparatorChar; + } + using var archive = encoding == null + ? ZipFile.OpenRead(sourceArchiveFile) + : ZipFile.Open(sourceArchiveFile, ZipArchiveMode.Read, encoding); + foreach (var entry in archive.Entries) + { + var destinationPath = Path.GetFullPath(Path.Combine(destinationRoot, entry.FullName)); + if (!destinationPath.StartsWith(normalizedRoot, StringComparison.Ordinal)) + { + throw new IOException("Extracting Zip entry would have resulted in a file outside the specified destination directory."); + } + if (entry.Name.Length == 0) + { + Directory.CreateDirectory(destinationPath); + continue; + } + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + entry.ExtractToFile(destinationPath, overwrite: true); } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); } /// /// Asynchronously opens a ZipArchive on the specified archiveFileName in the specified ZipArchiveMode mode. diff --git a/src/Split/net8.0/ConvertPolyfill.cs b/src/Split/net8.0/ConvertPolyfill.cs index a839f8dae..34e795be6 100644 --- a/src/Split/net8.0/ConvertPolyfill.cs +++ b/src/Split/net8.0/ConvertPolyfill.cs @@ -30,19 +30,19 @@ public static OperationStatus FromHexString(ReadOnlySpan source, Span= destination.Length) { charsConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal(source[i]); + var lo = GetHexVal(source[i + 1]); + if (hi < 0 || lo < 0) { - charsConsumed = i; + charsConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; @@ -89,19 +89,19 @@ public static OperationStatus FromHexString(ReadOnlySpan utf8Source, Span< var j = 0; while (i + 1 < utf8Source.Length) { - var hi = GetHexVal((char)utf8Source[i]); - var lo = GetHexVal((char)utf8Source[i + 1]); - if (hi < 0 || lo < 0) + if (j >= destination.Length) { bytesConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal((char)utf8Source[i]); + var lo = GetHexVal((char)utf8Source[i + 1]); + if (hi < 0 || lo < 0) { - bytesConsumed = i; + bytesConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; diff --git a/src/Split/net8.0/EqualityComparerPolyfill.cs b/src/Split/net8.0/EqualityComparerPolyfill.cs index 41245747e..035fc238b 100644 --- a/src/Split/net8.0/EqualityComparerPolyfill.cs +++ b/src/Split/net8.0/EqualityComparerPolyfill.cs @@ -24,7 +24,13 @@ public override int GetHashCode(T obj) /// public static EqualityComparer Create( Func keySelector, - IEqualityComparer? keyComparer = null) => - new KeySelectorEqualityComparer(keySelector, keyComparer); + IEqualityComparer? keyComparer = null) + { + if (keySelector is null) + { + throw new ArgumentNullException(nameof(keySelector)); + } + return new KeySelectorEqualityComparer(keySelector, keyComparer); + } } } diff --git a/src/Split/net8.0/Polyfill.cs b/src/Split/net8.0/Polyfill.cs index 16e916262..b7b21af17 100644 --- a/src/Split/net8.0/Polyfill.cs +++ b/src/Split/net8.0/Polyfill.cs @@ -17,5 +17,5 @@ namespace Polyfills; #endif static partial class Polyfill { - const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond * 1000; + const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; } diff --git a/src/Split/net8.0/Polyfill_String.cs b/src/Split/net8.0/Polyfill_String.cs index dc72e38f7..99ec08a0c 100644 --- a/src/Split/net8.0/Polyfill_String.cs +++ b/src/Split/net8.0/Polyfill_String.cs @@ -3,8 +3,6 @@ namespace Polyfills; using System; using System.Text; -using System.IO; -using System.Runtime.CompilerServices; static partial class Polyfill { /// diff --git a/src/Split/net8.0/StringPolyfill.cs b/src/Split/net8.0/StringPolyfill.cs index 747f914bb..7d39ca017 100644 --- a/src/Split/net8.0/StringPolyfill.cs +++ b/src/Split/net8.0/StringPolyfill.cs @@ -50,6 +50,10 @@ public static string Join(char separator, scoped ReadOnlySpan values) span = span.Slice(1); } var value = values[index]; + if (value is null) + { + continue; + } value.CopyTo(span); span = span.Slice(value.Length); } diff --git a/src/Split/net8.0/TimeSpanPolyfill.cs b/src/Split/net8.0/TimeSpanPolyfill.cs index 0a4c8afd0..bad6237ed 100644 --- a/src/Split/net8.0/TimeSpanPolyfill.cs +++ b/src/Split/net8.0/TimeSpanPolyfill.cs @@ -5,52 +5,71 @@ namespace Polyfills; static partial class Polyfill { const long MicrosecondsToTicks = TimeSpan.TicksPerMillisecond / 1000; + const long MicrosecondsPerMillisecond = 1_000; + const long MicrosecondsPerSecond = 1_000_000; + const long MicrosecondsPerMinute = 60_000_000; + const long MicrosecondsPerHour = 3_600_000_000; + const long MicrosecondsPerDay = 86_400_000_000; + static TimeSpan TicksFromMicroseconds(decimal totalMicroseconds) + { + var ticks = totalMicroseconds * MicrosecondsToTicks; + if (ticks < long.MinValue || ticks > long.MaxValue) + { + throw new ArgumentOutOfRangeException(null, "TimeSpan overflowed because the duration is too long."); + } + return new((long)ticks); + } extension(TimeSpan) { /// /// Initializes a new instance of the structure to a specified number of days, hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromDays(int days, int hours = 0, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)days * TimeSpan.TicksPerDay + - (long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)days * MicrosecondsPerDay + + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromHours(int hours, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromMinutes(int minutes, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of seconds, milliseconds, and microseconds. /// public static TimeSpan FromSeconds(long seconds, long milliseconds = 0, long microseconds = 0) => - new(seconds * TimeSpan.TicksPerSecond + - milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of milliseconds and microseconds. /// public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0) => - new(milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of microseconds. /// public static TimeSpan FromMicroseconds(long microseconds) => - new(microseconds * MicrosecondsToTicks); + TicksFromMicroseconds(microseconds); } } diff --git a/src/Split/net9.0/ConvertPolyfill.cs b/src/Split/net9.0/ConvertPolyfill.cs index ec7bfc5fc..fb788bb19 100644 --- a/src/Split/net9.0/ConvertPolyfill.cs +++ b/src/Split/net9.0/ConvertPolyfill.cs @@ -37,19 +37,19 @@ public static OperationStatus FromHexString(ReadOnlySpan utf8Source, Span< var j = 0; while (i + 1 < utf8Source.Length) { - var hi = GetHexVal((char)utf8Source[i]); - var lo = GetHexVal((char)utf8Source[i + 1]); - if (hi < 0 || lo < 0) + if (j >= destination.Length) { bytesConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal((char)utf8Source[i]); + var lo = GetHexVal((char)utf8Source[i + 1]); + if (hi < 0 || lo < 0) { - bytesConsumed = i; + bytesConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; diff --git a/src/Split/net9.0/EqualityComparerPolyfill.cs b/src/Split/net9.0/EqualityComparerPolyfill.cs index 41245747e..035fc238b 100644 --- a/src/Split/net9.0/EqualityComparerPolyfill.cs +++ b/src/Split/net9.0/EqualityComparerPolyfill.cs @@ -24,7 +24,13 @@ public override int GetHashCode(T obj) /// public static EqualityComparer Create( Func keySelector, - IEqualityComparer? keyComparer = null) => - new KeySelectorEqualityComparer(keySelector, keyComparer); + IEqualityComparer? keyComparer = null) + { + if (keySelector is null) + { + throw new ArgumentNullException(nameof(keySelector)); + } + return new KeySelectorEqualityComparer(keySelector, keyComparer); + } } } diff --git a/src/Split/net9.0/Polyfill.cs b/src/Split/net9.0/Polyfill.cs index 16e916262..b7b21af17 100644 --- a/src/Split/net9.0/Polyfill.cs +++ b/src/Split/net9.0/Polyfill.cs @@ -17,5 +17,5 @@ namespace Polyfills; #endif static partial class Polyfill { - const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond * 1000; + const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; } diff --git a/src/Split/net9.0/Polyfill_String.cs b/src/Split/net9.0/Polyfill_String.cs index dc72e38f7..99ec08a0c 100644 --- a/src/Split/net9.0/Polyfill_String.cs +++ b/src/Split/net9.0/Polyfill_String.cs @@ -3,8 +3,6 @@ namespace Polyfills; using System; using System.Text; -using System.IO; -using System.Runtime.CompilerServices; static partial class Polyfill { /// diff --git a/src/Split/netcoreapp2.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs b/src/Split/netcoreapp2.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs index 32b0afded..8bad1d83d 100644 --- a/src/Split/netcoreapp2.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs +++ b/src/Split/netcoreapp2.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs @@ -15,7 +15,7 @@ public static void ThrowIfZero(T value, [CallerArgumentExpression(nameof(valu { if (value.Equals(default)) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } /// Throws an if is zero. @@ -23,12 +23,12 @@ public static void ThrowIfZero(nint value, [CallerArgumentExpression(nameof(valu { if (value == 0) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } [DoesNotReturn] - static void ThrowZero(string? paramName) => - throw new ArgumentOutOfRangeException(paramName, "Value must not be zero."); + static void ThrowZero(T value, string? paramName) => + throw new ArgumentOutOfRangeException(paramName, value, $"{paramName} ('{value}') must be a non-zero value."); public static void ThrowIfNegative(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : struct, IComparable { @@ -112,7 +112,7 @@ public static void ThrowIfGreaterThanOrEqual(nint value, nint other, [CallerArgu { if (value >= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than {other}."); } } public static void ThrowIfLessThan(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) @@ -144,7 +144,7 @@ public static void ThrowIfLessThanOrEqual(nint value, nint other, [CallerArgumen { if (value <= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than {other}."); } } } diff --git a/src/Split/netcoreapp2.0/ConvertPolyfill.cs b/src/Split/netcoreapp2.0/ConvertPolyfill.cs index 412674710..1f45391f9 100644 --- a/src/Split/netcoreapp2.0/ConvertPolyfill.cs +++ b/src/Split/netcoreapp2.0/ConvertPolyfill.cs @@ -134,19 +134,19 @@ public static OperationStatus FromHexString(ReadOnlySpan source, Span= destination.Length) { charsConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal(source[i]); + var lo = GetHexVal(source[i + 1]); + if (hi < 0 || lo < 0) { - charsConsumed = i; + charsConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; @@ -193,19 +193,19 @@ public static OperationStatus FromHexString(ReadOnlySpan utf8Source, Span< var j = 0; while (i + 1 < utf8Source.Length) { - var hi = GetHexVal((char)utf8Source[i]); - var lo = GetHexVal((char)utf8Source[i + 1]); - if (hi < 0 || lo < 0) + if (j >= destination.Length) { bytesConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal((char)utf8Source[i]); + var lo = GetHexVal((char)utf8Source[i + 1]); + if (hi < 0 || lo < 0) { - bytesConsumed = i; + bytesConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; diff --git a/src/Split/netcoreapp2.0/DateTimeOffsetPolyfill.cs b/src/Split/netcoreapp2.0/DateTimeOffsetPolyfill.cs index 6837790e5..012b944dc 100644 --- a/src/Split/netcoreapp2.0/DateTimeOffsetPolyfill.cs +++ b/src/Split/netcoreapp2.0/DateTimeOffsetPolyfill.cs @@ -17,7 +17,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTimeOffset(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Offset); diff --git a/src/Split/netcoreapp2.0/DateTimePolyfill.cs b/src/Split/netcoreapp2.0/DateTimePolyfill.cs index 4641f3e59..430683e14 100644 --- a/src/Split/netcoreapp2.0/DateTimePolyfill.cs +++ b/src/Split/netcoreapp2.0/DateTimePolyfill.cs @@ -16,7 +16,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTime(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Kind); diff --git a/src/Split/netcoreapp2.0/EqualityComparerPolyfill.cs b/src/Split/netcoreapp2.0/EqualityComparerPolyfill.cs index e45f2a30e..55cb3ad44 100644 --- a/src/Split/netcoreapp2.0/EqualityComparerPolyfill.cs +++ b/src/Split/netcoreapp2.0/EqualityComparerPolyfill.cs @@ -19,8 +19,14 @@ public override int GetHashCode(T obj) => /// public static EqualityComparer Create( Func equals, - Func? getHashCode = null) => - new DelegateEqualityComparer(equals, getHashCode); + Func? getHashCode = null) + { + if (equals is null) + { + throw new ArgumentNullException(nameof(equals)); + } + return new DelegateEqualityComparer(equals, getHashCode); + } } class KeySelectorEqualityComparer(Func keySelector, IEqualityComparer? keyComparer) : EqualityComparer @@ -41,7 +47,13 @@ public override int GetHashCode(T obj) /// public static EqualityComparer Create( Func keySelector, - IEqualityComparer? keyComparer = null) => - new KeySelectorEqualityComparer(keySelector, keyComparer); + IEqualityComparer? keyComparer = null) + { + if (keySelector is null) + { + throw new ArgumentNullException(nameof(keySelector)); + } + return new KeySelectorEqualityComparer(keySelector, keyComparer); + } } } diff --git a/src/Split/netcoreapp2.0/FileUnixModePolyfill.cs b/src/Split/netcoreapp2.0/FileUnixModePolyfill.cs index 2440f7800..701ef07cb 100644 --- a/src/Split/netcoreapp2.0/FileUnixModePolyfill.cs +++ b/src/Split/netcoreapp2.0/FileUnixModePolyfill.cs @@ -47,8 +47,8 @@ public static UnixFileMode GetUnixFileMode(string path) var mode = output[0] switch { '1' => UnixFileMode.StickyBit, - '2' => UnixFileMode.SetUser, - '4' => UnixFileMode.SetGroup, + '2' => UnixFileMode.SetGroup, + '4' => UnixFileMode.SetUser, _ => UnixFileMode.None }; mode = output[1] switch @@ -158,13 +158,13 @@ public static void SetUnixFileMode(string path, UnixFileMode mode) octal[3] += 1; break; case "setuser": - octal[0] += 2; + octal[0] += 4; break; case "stickybit": octal[0] += 1; break; case "setgroup": - octal[0] += 4; + octal[0] += 2; break; default: throw new("Invalid notation detected"); diff --git a/src/Split/netcoreapp2.0/Numbers/DoublePolyfill.cs b/src/Split/netcoreapp2.0/Numbers/DoublePolyfill.cs index ba5333f69..2e3e40efd 100644 --- a/src/Split/netcoreapp2.0/Numbers/DoublePolyfill.cs +++ b/src/Split/netcoreapp2.0/Numbers/DoublePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out double result) => - double.TryParse(s, NumberStyles.Float, provider, out result); + double.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,7 +28,7 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its double-precision floating-point number equivalent.. /// public static bool TryParse(ReadOnlySpan utf8Text, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Converts the span representation of a number in a specified style and culture-specific format to its double-precision floating-point number equivalent. A return value indicates whether the conversion succeeded or failed. /// @@ -43,7 +43,7 @@ public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatPro /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out double result) => - double.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + double.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/netcoreapp2.0/Numbers/SinglePolyfill.cs b/src/Split/netcoreapp2.0/Numbers/SinglePolyfill.cs index 69af78095..2f6e02a82 100644 --- a/src/Split/netcoreapp2.0/Numbers/SinglePolyfill.cs +++ b/src/Split/netcoreapp2.0/Numbers/SinglePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out float result) => - float.TryParse(s, NumberStyles.Float, provider, out result); + float.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,7 +28,7 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its single-precision floating-point number equivalent. /// public static bool TryParse(ReadOnlySpan utf8Text, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Converts the span representation of a number in a specified style and culture-specific format to its single-precision floating-point number equivalent. A return value indicates whether the conversion succeeded or failed. /// @@ -43,7 +43,7 @@ public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatPro /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out float result) => - float.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + float.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/netcoreapp2.0/Polyfill.cs b/src/Split/netcoreapp2.0/Polyfill.cs index 16e916262..b7b21af17 100644 --- a/src/Split/netcoreapp2.0/Polyfill.cs +++ b/src/Split/netcoreapp2.0/Polyfill.cs @@ -17,5 +17,5 @@ namespace Polyfills; #endif static partial class Polyfill { - const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond * 1000; + const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; } diff --git a/src/Split/netcoreapp2.0/Polyfill_Encoding_GetChars.cs b/src/Split/netcoreapp2.0/Polyfill_Encoding_GetChars.cs index f5a7de80b..4b956e845 100644 --- a/src/Split/netcoreapp2.0/Polyfill_Encoding_GetChars.cs +++ b/src/Split/netcoreapp2.0/Polyfill_Encoding_GetChars.cs @@ -25,7 +25,7 @@ public static int GetChars(this Encoding target, ReadOnlySpan bytes, Span< var charArray = new char[bytes.Length]; var array = bytes.ToArray(); var count = target.GetChars(array, 0, bytes.Length, charArray, 0); - new ReadOnlySpan(charArray).CopyTo(chars); + new ReadOnlySpan(charArray, 0, count).CopyTo(chars); return count; } #endif diff --git a/src/Split/netcoreapp2.0/Polyfill_List.cs b/src/Split/netcoreapp2.0/Polyfill_List.cs index c2e76ed77..7a9348d0c 100644 --- a/src/Split/netcoreapp2.0/Polyfill_List.cs +++ b/src/Split/netcoreapp2.0/Polyfill_List.cs @@ -21,6 +21,10 @@ public static void AddRange(this List target, ReadOnlySpan source) /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { + if ((uint)index > (uint)target.Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } for (var i = 0; i < source.Length; i++) { var item = source[i]; @@ -30,6 +34,10 @@ public static void InsertRange(this List target, int index, ReadOnlySpanCopies the entire to a span. public static void CopyTo(this List target, Span destination) { + if (target.Count > destination.Length) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } for (var index = 0; index < target.Count; index++) { destination[index] = target[index]; diff --git a/src/Split/netcoreapp2.0/Polyfill_Memory_SpanSort.cs b/src/Split/netcoreapp2.0/Polyfill_Memory_SpanSort.cs index acc5c8666..b30957972 100644 --- a/src/Split/netcoreapp2.0/Polyfill_Memory_SpanSort.cs +++ b/src/Split/netcoreapp2.0/Polyfill_Memory_SpanSort.cs @@ -77,12 +77,8 @@ internal ComparerWrapper(Comparison comparison) throw new ArgumentNullException(nameof(comparison)); this.comparison = comparison; } - public int Compare(T? x, T? y) - { - if (x is null || y is null) - return 0; - return comparison((T)x, (T)y); - } + public int Compare(T? x, T? y) => + comparison(x!, y!); } } #endif diff --git a/src/Split/netcoreapp2.0/Polyfill_MicroNanosecondAdd.cs b/src/Split/netcoreapp2.0/Polyfill_MicroNanosecondAdd.cs index 17d2c8d00..e099e1260 100644 --- a/src/Split/netcoreapp2.0/Polyfill_MicroNanosecondAdd.cs +++ b/src/Split/netcoreapp2.0/Polyfill_MicroNanosecondAdd.cs @@ -8,10 +8,10 @@ static partial class Polyfill /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTime AddMicroseconds(this DateTime target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); /// /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTimeOffset AddMicroseconds(this DateTimeOffset target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); } diff --git a/src/Split/netcoreapp2.0/Polyfill_String.cs b/src/Split/netcoreapp2.0/Polyfill_String.cs index 26afdec8b..653d44cf4 100644 --- a/src/Split/netcoreapp2.0/Polyfill_String.cs +++ b/src/Split/netcoreapp2.0/Polyfill_String.cs @@ -3,8 +3,6 @@ namespace Polyfills; using System; using System.Text; -using System.IO; -using System.Runtime.CompilerServices; static partial class Polyfill { #if FeatureMemory @@ -71,22 +69,49 @@ public static int LastIndexOf(this string target, char value, int startIndex, in /// /// Replaces all newline sequences in the current string with . /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ReplaceLineEndings(this string target, string replacementText) { + var index = IndexOfNewlineChar(target, 0, out var stride); + if (index < 0) + { + return target; + } var builder = new StringBuilder(target.Length); - using var reader = new StringReader(target); + var position = 0; while (true) { - var line = reader.ReadLine(); - if (line == null) + builder.Append(target, position, index - position); + builder.Append(replacementText); + position = index + stride; + index = IndexOfNewlineChar(target, position, out stride); + if (index < 0) { break; } - builder.Append(line); - builder.Append(replacementText); } - return builder.ToString(0, builder.Length - replacementText.Length); + builder.Append(target, position, target.Length - position); + return builder.ToString(); + } + static int IndexOfNewlineChar(string text, int startIndex, out int stride) + { + stride = 0; + for (var index = startIndex; index < text.Length; index++) + { + switch (text[index]) + { + case '\r': + stride = index + 1 < text.Length && text[index + 1] == '\n' ? 2 : 1; + return index; + case '\n': + case '\f': + case '\u0085': + case '\u2028': + case '\u2029': + stride = 1; + return index; + } + } + return -1; } /// /// Replaces all newline sequences in the current string with . diff --git a/src/Split/netcoreapp2.0/Polyfill_StringBuilder_Append.cs b/src/Split/netcoreapp2.0/Polyfill_StringBuilder_Append.cs index 0b60749a9..d3970ce53 100644 --- a/src/Split/netcoreapp2.0/Polyfill_StringBuilder_Append.cs +++ b/src/Split/netcoreapp2.0/Polyfill_StringBuilder_Append.cs @@ -10,6 +10,14 @@ static partial class Polyfill /// public static StringBuilder Append(this StringBuilder target, StringBuilder? value, int startIndex, int count) { + if (startIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(startIndex)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } if (value == null) { if (startIndex == 0 && count == 0) diff --git a/src/Split/netcoreapp2.0/Polyfill_StringBuilder_CopyTo.cs b/src/Split/netcoreapp2.0/Polyfill_StringBuilder_CopyTo.cs index dc625a7bc..ba99e0a1f 100644 --- a/src/Split/netcoreapp2.0/Polyfill_StringBuilder_CopyTo.cs +++ b/src/Split/netcoreapp2.0/Polyfill_StringBuilder_CopyTo.cs @@ -15,20 +15,25 @@ public static void CopyTo( Span destination, int count) { - var destinationIndex = 0; - while (true) + if (count < 0) { - if (sourceIndex == target.Length) - { - break; - } - if (destinationIndex == count) - { - break; - } - destination[destinationIndex] = target[sourceIndex]; - destinationIndex++; - sourceIndex++; + throw new ArgumentOutOfRangeException(nameof(count)); + } + if ((uint)sourceIndex > (uint)target.Length) + { + throw new ArgumentOutOfRangeException(nameof(sourceIndex)); + } + if (sourceIndex > target.Length - count) + { + throw new ArgumentException("The source StringBuilder does not contain enough characters from sourceIndex onward to satisfy count."); + } + if (count > destination.Length) + { + throw new ArgumentException("The destination span is too short to hold the requested characters.", nameof(destination)); + } + for (var index = 0; index < count; index++) + { + destination[index] = target[sourceIndex + index]; } } } diff --git a/src/Split/netcoreapp2.0/Polyfill_Type.cs b/src/Split/netcoreapp2.0/Polyfill_Type.cs index 7890e2168..b99bdfab5 100644 --- a/src/Split/netcoreapp2.0/Polyfill_Type.cs +++ b/src/Split/netcoreapp2.0/Polyfill_Type.cs @@ -7,9 +7,15 @@ namespace Polyfills; using System.Linq; static partial class Polyfill { - public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInfo other) => - target.MetadataToken == other.MetadataToken && - target.Module.Equals(other.Module); + public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInfo other) + { + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } + return target.MetadataToken == other.MetadataToken && + target.Module.Equals(other.Module); + } /// /// Searches for the specified method whose parameters match the specified generic parameter count, argument types and modifiers, using the specified binding constraints. /// @@ -22,6 +28,25 @@ public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInf Type[] types, ParameterModifier[]? modifiers) { + if (name is null) + { + throw new ArgumentNullException(nameof(name)); + } + if (genericParameterCount < 0) + { + throw new ArgumentOutOfRangeException(nameof(genericParameterCount)); + } + if (types is null) + { + throw new ArgumentNullException(nameof(types)); + } + foreach (var type in types) + { + if (type is null) + { + throw new ArgumentNullException(nameof(types)); + } + } var methods = target.GetMethods(bindingAttr); if (genericParameterCount == 0) { diff --git a/src/Split/netcoreapp2.0/RandomNumberGeneratorPolyfill.cs b/src/Split/netcoreapp2.0/RandomNumberGeneratorPolyfill.cs index 08384a7ea..539aa7aba 100644 --- a/src/Split/netcoreapp2.0/RandomNumberGeneratorPolyfill.cs +++ b/src/Split/netcoreapp2.0/RandomNumberGeneratorPolyfill.cs @@ -34,7 +34,7 @@ public static int GetInt32(int fromInclusive, int toExclusive) { generator.GetBytes(bytes); value = BitConverter.ToUInt32(bytes, 0) & mask; - } while (value >= range); + } while (value > range); return (int) (fromInclusive + value); } /// diff --git a/src/Split/netcoreapp2.0/StringPolyfill.cs b/src/Split/netcoreapp2.0/StringPolyfill.cs index 6ae5df100..6412adfc4 100644 --- a/src/Split/netcoreapp2.0/StringPolyfill.cs +++ b/src/Split/netcoreapp2.0/StringPolyfill.cs @@ -50,6 +50,10 @@ public static string Join(char separator, scoped ReadOnlySpan values) span = span.Slice(1); } var value = values[index]; + if (value is null) + { + continue; + } value.CopyTo(span); span = span.Slice(value.Length); } diff --git a/src/Split/netcoreapp2.0/TaskCompletionSource.cs b/src/Split/netcoreapp2.0/TaskCompletionSource.cs index 04b507f76..67a35f938 100644 --- a/src/Split/netcoreapp2.0/TaskCompletionSource.cs +++ b/src/Split/netcoreapp2.0/TaskCompletionSource.cs @@ -77,5 +77,5 @@ public TaskCompletionSource(object? state, TaskCreationOptions creationOptions) /// /// Attempts to transition the underlying into the state. /// - public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(default); + public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(cancellationToken); } diff --git a/src/Split/netcoreapp2.0/TimeSpanPolyfill.cs b/src/Split/netcoreapp2.0/TimeSpanPolyfill.cs index 94d13cbbb..59d7c7974 100644 --- a/src/Split/netcoreapp2.0/TimeSpanPolyfill.cs +++ b/src/Split/netcoreapp2.0/TimeSpanPolyfill.cs @@ -15,7 +15,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microseconds => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new TimeSpan(target.Days, target.Hours, target.Minutes, 0); @@ -24,52 +24,71 @@ long TicksComponent() } } const long MicrosecondsToTicks = TimeSpan.TicksPerMillisecond / 1000; + const long MicrosecondsPerMillisecond = 1_000; + const long MicrosecondsPerSecond = 1_000_000; + const long MicrosecondsPerMinute = 60_000_000; + const long MicrosecondsPerHour = 3_600_000_000; + const long MicrosecondsPerDay = 86_400_000_000; + static TimeSpan TicksFromMicroseconds(decimal totalMicroseconds) + { + var ticks = totalMicroseconds * MicrosecondsToTicks; + if (ticks < long.MinValue || ticks > long.MaxValue) + { + throw new ArgumentOutOfRangeException(null, "TimeSpan overflowed because the duration is too long."); + } + return new((long)ticks); + } extension(TimeSpan) { /// /// Initializes a new instance of the structure to a specified number of days, hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromDays(int days, int hours = 0, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)days * TimeSpan.TicksPerDay + - (long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)days * MicrosecondsPerDay + + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromHours(int hours, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromMinutes(int minutes, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of seconds, milliseconds, and microseconds. /// public static TimeSpan FromSeconds(long seconds, long milliseconds = 0, long microseconds = 0) => - new(seconds * TimeSpan.TicksPerSecond + - milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of milliseconds and microseconds. /// public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0) => - new(milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of microseconds. /// public static TimeSpan FromMicroseconds(long microseconds) => - new(microseconds * MicrosecondsToTicks); + TicksFromMicroseconds(microseconds); } } diff --git a/src/Split/netcoreapp2.0/ZipFilePolyfill.cs b/src/Split/netcoreapp2.0/ZipFilePolyfill.cs index 6cb361e6f..d908c5633 100644 --- a/src/Split/netcoreapp2.0/ZipFilePolyfill.cs +++ b/src/Split/netcoreapp2.0/ZipFilePolyfill.cs @@ -53,11 +53,12 @@ static void ExtractToDirectoryPolyfill( string destinationDirectory, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + return; } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, null); } static void ExtractToDirectoryPolyfill( string sourceArchiveFile, @@ -65,11 +66,43 @@ static void ExtractToDirectoryPolyfill( Encoding encoding, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); + return; + } + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, encoding); + } + static void ExtractToDirectoryWithOverwrite( + string sourceArchiveFile, + string destinationDirectory, + Encoding? encoding) + { + var destinationRoot = Path.GetFullPath(destinationDirectory); + Directory.CreateDirectory(destinationRoot); + var normalizedRoot = destinationRoot; + if (normalizedRoot[normalizedRoot.Length - 1] != Path.DirectorySeparatorChar) + { + normalizedRoot += Path.DirectorySeparatorChar; + } + using var archive = encoding == null + ? ZipFile.OpenRead(sourceArchiveFile) + : ZipFile.Open(sourceArchiveFile, ZipArchiveMode.Read, encoding); + foreach (var entry in archive.Entries) + { + var destinationPath = Path.GetFullPath(Path.Combine(destinationRoot, entry.FullName)); + if (!destinationPath.StartsWith(normalizedRoot, StringComparison.Ordinal)) + { + throw new IOException("Extracting Zip entry would have resulted in a file outside the specified destination directory."); + } + if (entry.Name.Length == 0) + { + Directory.CreateDirectory(destinationPath); + continue; + } + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + entry.ExtractToFile(destinationPath, overwrite: true); } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); } /// /// Asynchronously opens a ZipArchive on the specified archiveFileName in the specified ZipArchiveMode mode. diff --git a/src/Split/netcoreapp2.1/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs b/src/Split/netcoreapp2.1/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs index 32b0afded..8bad1d83d 100644 --- a/src/Split/netcoreapp2.1/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs +++ b/src/Split/netcoreapp2.1/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs @@ -15,7 +15,7 @@ public static void ThrowIfZero(T value, [CallerArgumentExpression(nameof(valu { if (value.Equals(default)) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } /// Throws an if is zero. @@ -23,12 +23,12 @@ public static void ThrowIfZero(nint value, [CallerArgumentExpression(nameof(valu { if (value == 0) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } [DoesNotReturn] - static void ThrowZero(string? paramName) => - throw new ArgumentOutOfRangeException(paramName, "Value must not be zero."); + static void ThrowZero(T value, string? paramName) => + throw new ArgumentOutOfRangeException(paramName, value, $"{paramName} ('{value}') must be a non-zero value."); public static void ThrowIfNegative(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : struct, IComparable { @@ -112,7 +112,7 @@ public static void ThrowIfGreaterThanOrEqual(nint value, nint other, [CallerArgu { if (value >= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than {other}."); } } public static void ThrowIfLessThan(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) @@ -144,7 +144,7 @@ public static void ThrowIfLessThanOrEqual(nint value, nint other, [CallerArgumen { if (value <= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than {other}."); } } } diff --git a/src/Split/netcoreapp2.1/ConvertPolyfill.cs b/src/Split/netcoreapp2.1/ConvertPolyfill.cs index c7f6719c7..f3f76fb55 100644 --- a/src/Split/netcoreapp2.1/ConvertPolyfill.cs +++ b/src/Split/netcoreapp2.1/ConvertPolyfill.cs @@ -73,19 +73,19 @@ public static OperationStatus FromHexString(ReadOnlySpan source, Span= destination.Length) { charsConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal(source[i]); + var lo = GetHexVal(source[i + 1]); + if (hi < 0 || lo < 0) { - charsConsumed = i; + charsConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; @@ -132,19 +132,19 @@ public static OperationStatus FromHexString(ReadOnlySpan utf8Source, Span< var j = 0; while (i + 1 < utf8Source.Length) { - var hi = GetHexVal((char)utf8Source[i]); - var lo = GetHexVal((char)utf8Source[i + 1]); - if (hi < 0 || lo < 0) + if (j >= destination.Length) { bytesConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal((char)utf8Source[i]); + var lo = GetHexVal((char)utf8Source[i + 1]); + if (hi < 0 || lo < 0) { - bytesConsumed = i; + bytesConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; diff --git a/src/Split/netcoreapp2.1/DateTimeOffsetPolyfill.cs b/src/Split/netcoreapp2.1/DateTimeOffsetPolyfill.cs index 5a5018208..eb423b73a 100644 --- a/src/Split/netcoreapp2.1/DateTimeOffsetPolyfill.cs +++ b/src/Split/netcoreapp2.1/DateTimeOffsetPolyfill.cs @@ -17,7 +17,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTimeOffset(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Offset); diff --git a/src/Split/netcoreapp2.1/DateTimePolyfill.cs b/src/Split/netcoreapp2.1/DateTimePolyfill.cs index 4641f3e59..430683e14 100644 --- a/src/Split/netcoreapp2.1/DateTimePolyfill.cs +++ b/src/Split/netcoreapp2.1/DateTimePolyfill.cs @@ -16,7 +16,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTime(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Kind); diff --git a/src/Split/netcoreapp2.1/EqualityComparerPolyfill.cs b/src/Split/netcoreapp2.1/EqualityComparerPolyfill.cs index e45f2a30e..55cb3ad44 100644 --- a/src/Split/netcoreapp2.1/EqualityComparerPolyfill.cs +++ b/src/Split/netcoreapp2.1/EqualityComparerPolyfill.cs @@ -19,8 +19,14 @@ public override int GetHashCode(T obj) => /// public static EqualityComparer Create( Func equals, - Func? getHashCode = null) => - new DelegateEqualityComparer(equals, getHashCode); + Func? getHashCode = null) + { + if (equals is null) + { + throw new ArgumentNullException(nameof(equals)); + } + return new DelegateEqualityComparer(equals, getHashCode); + } } class KeySelectorEqualityComparer(Func keySelector, IEqualityComparer? keyComparer) : EqualityComparer @@ -41,7 +47,13 @@ public override int GetHashCode(T obj) /// public static EqualityComparer Create( Func keySelector, - IEqualityComparer? keyComparer = null) => - new KeySelectorEqualityComparer(keySelector, keyComparer); + IEqualityComparer? keyComparer = null) + { + if (keySelector is null) + { + throw new ArgumentNullException(nameof(keySelector)); + } + return new KeySelectorEqualityComparer(keySelector, keyComparer); + } } } diff --git a/src/Split/netcoreapp2.1/FileUnixModePolyfill.cs b/src/Split/netcoreapp2.1/FileUnixModePolyfill.cs index 2440f7800..701ef07cb 100644 --- a/src/Split/netcoreapp2.1/FileUnixModePolyfill.cs +++ b/src/Split/netcoreapp2.1/FileUnixModePolyfill.cs @@ -47,8 +47,8 @@ public static UnixFileMode GetUnixFileMode(string path) var mode = output[0] switch { '1' => UnixFileMode.StickyBit, - '2' => UnixFileMode.SetUser, - '4' => UnixFileMode.SetGroup, + '2' => UnixFileMode.SetGroup, + '4' => UnixFileMode.SetUser, _ => UnixFileMode.None }; mode = output[1] switch @@ -158,13 +158,13 @@ public static void SetUnixFileMode(string path, UnixFileMode mode) octal[3] += 1; break; case "setuser": - octal[0] += 2; + octal[0] += 4; break; case "stickybit": octal[0] += 1; break; case "setgroup": - octal[0] += 4; + octal[0] += 2; break; default: throw new("Invalid notation detected"); diff --git a/src/Split/netcoreapp2.1/Numbers/DoublePolyfill.cs b/src/Split/netcoreapp2.1/Numbers/DoublePolyfill.cs index ff9f8aca6..d08a8d3b6 100644 --- a/src/Split/netcoreapp2.1/Numbers/DoublePolyfill.cs +++ b/src/Split/netcoreapp2.1/Numbers/DoublePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out double result) => - double.TryParse(s, NumberStyles.Float, provider, out result); + double.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,12 +28,12 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its double-precision floating-point number equivalent.. /// public static bool TryParse(ReadOnlySpan utf8Text, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out double result) => - double.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + double.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/netcoreapp2.1/Numbers/SinglePolyfill.cs b/src/Split/netcoreapp2.1/Numbers/SinglePolyfill.cs index 3d574e6c7..6a49cbf6a 100644 --- a/src/Split/netcoreapp2.1/Numbers/SinglePolyfill.cs +++ b/src/Split/netcoreapp2.1/Numbers/SinglePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out float result) => - float.TryParse(s, NumberStyles.Float, provider, out result); + float.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,12 +28,12 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its single-precision floating-point number equivalent. /// public static bool TryParse(ReadOnlySpan utf8Text, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out float result) => - float.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + float.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/netcoreapp2.1/Polyfill.cs b/src/Split/netcoreapp2.1/Polyfill.cs index 16e916262..b7b21af17 100644 --- a/src/Split/netcoreapp2.1/Polyfill.cs +++ b/src/Split/netcoreapp2.1/Polyfill.cs @@ -17,5 +17,5 @@ namespace Polyfills; #endif static partial class Polyfill { - const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond * 1000; + const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; } diff --git a/src/Split/netcoreapp2.1/Polyfill_List.cs b/src/Split/netcoreapp2.1/Polyfill_List.cs index c2e76ed77..7a9348d0c 100644 --- a/src/Split/netcoreapp2.1/Polyfill_List.cs +++ b/src/Split/netcoreapp2.1/Polyfill_List.cs @@ -21,6 +21,10 @@ public static void AddRange(this List target, ReadOnlySpan source) /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { + if ((uint)index > (uint)target.Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } for (var i = 0; i < source.Length; i++) { var item = source[i]; @@ -30,6 +34,10 @@ public static void InsertRange(this List target, int index, ReadOnlySpanCopies the entire to a span. public static void CopyTo(this List target, Span destination) { + if (target.Count > destination.Length) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } for (var index = 0; index < target.Count; index++) { destination[index] = target[index]; diff --git a/src/Split/netcoreapp2.1/Polyfill_Memory_SpanSort.cs b/src/Split/netcoreapp2.1/Polyfill_Memory_SpanSort.cs index acc5c8666..b30957972 100644 --- a/src/Split/netcoreapp2.1/Polyfill_Memory_SpanSort.cs +++ b/src/Split/netcoreapp2.1/Polyfill_Memory_SpanSort.cs @@ -77,12 +77,8 @@ internal ComparerWrapper(Comparison comparison) throw new ArgumentNullException(nameof(comparison)); this.comparison = comparison; } - public int Compare(T? x, T? y) - { - if (x is null || y is null) - return 0; - return comparison((T)x, (T)y); - } + public int Compare(T? x, T? y) => + comparison(x!, y!); } } #endif diff --git a/src/Split/netcoreapp2.1/Polyfill_MicroNanosecondAdd.cs b/src/Split/netcoreapp2.1/Polyfill_MicroNanosecondAdd.cs index 17d2c8d00..e099e1260 100644 --- a/src/Split/netcoreapp2.1/Polyfill_MicroNanosecondAdd.cs +++ b/src/Split/netcoreapp2.1/Polyfill_MicroNanosecondAdd.cs @@ -8,10 +8,10 @@ static partial class Polyfill /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTime AddMicroseconds(this DateTime target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); /// /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTimeOffset AddMicroseconds(this DateTimeOffset target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); } diff --git a/src/Split/netcoreapp2.1/Polyfill_String.cs b/src/Split/netcoreapp2.1/Polyfill_String.cs index 2f9a508fd..bb7f8cfd4 100644 --- a/src/Split/netcoreapp2.1/Polyfill_String.cs +++ b/src/Split/netcoreapp2.1/Polyfill_String.cs @@ -3,8 +3,6 @@ namespace Polyfills; using System; using System.Text; -using System.IO; -using System.Runtime.CompilerServices; static partial class Polyfill { #if FeatureMemory @@ -66,22 +64,49 @@ public static int LastIndexOf(this string target, char value, int startIndex, in /// /// Replaces all newline sequences in the current string with . /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ReplaceLineEndings(this string target, string replacementText) { + var index = IndexOfNewlineChar(target, 0, out var stride); + if (index < 0) + { + return target; + } var builder = new StringBuilder(target.Length); - using var reader = new StringReader(target); + var position = 0; while (true) { - var line = reader.ReadLine(); - if (line == null) + builder.Append(target, position, index - position); + builder.Append(replacementText); + position = index + stride; + index = IndexOfNewlineChar(target, position, out stride); + if (index < 0) { break; } - builder.Append(line); - builder.Append(replacementText); } - return builder.ToString(0, builder.Length - replacementText.Length); + builder.Append(target, position, target.Length - position); + return builder.ToString(); + } + static int IndexOfNewlineChar(string text, int startIndex, out int stride) + { + stride = 0; + for (var index = startIndex; index < text.Length; index++) + { + switch (text[index]) + { + case '\r': + stride = index + 1 < text.Length && text[index + 1] == '\n' ? 2 : 1; + return index; + case '\n': + case '\f': + case '\u0085': + case '\u2028': + case '\u2029': + stride = 1; + return index; + } + } + return -1; } /// /// Replaces all newline sequences in the current string with . diff --git a/src/Split/netcoreapp2.1/RandomNumberGeneratorPolyfill.cs b/src/Split/netcoreapp2.1/RandomNumberGeneratorPolyfill.cs index c8b772fab..e90c042f9 100644 --- a/src/Split/netcoreapp2.1/RandomNumberGeneratorPolyfill.cs +++ b/src/Split/netcoreapp2.1/RandomNumberGeneratorPolyfill.cs @@ -34,7 +34,7 @@ public static int GetInt32(int fromInclusive, int toExclusive) { generator.GetBytes(bytes); value = BitConverter.ToUInt32(bytes, 0) & mask; - } while (value >= range); + } while (value > range); return (int) (fromInclusive + value); } /// diff --git a/src/Split/netcoreapp2.1/StringPolyfill.cs b/src/Split/netcoreapp2.1/StringPolyfill.cs index 6ae5df100..6412adfc4 100644 --- a/src/Split/netcoreapp2.1/StringPolyfill.cs +++ b/src/Split/netcoreapp2.1/StringPolyfill.cs @@ -50,6 +50,10 @@ public static string Join(char separator, scoped ReadOnlySpan values) span = span.Slice(1); } var value = values[index]; + if (value is null) + { + continue; + } value.CopyTo(span); span = span.Slice(value.Length); } diff --git a/src/Split/netcoreapp2.1/TaskCompletionSource.cs b/src/Split/netcoreapp2.1/TaskCompletionSource.cs index 04b507f76..67a35f938 100644 --- a/src/Split/netcoreapp2.1/TaskCompletionSource.cs +++ b/src/Split/netcoreapp2.1/TaskCompletionSource.cs @@ -77,5 +77,5 @@ public TaskCompletionSource(object? state, TaskCreationOptions creationOptions) /// /// Attempts to transition the underlying into the state. /// - public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(default); + public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(cancellationToken); } diff --git a/src/Split/netcoreapp2.1/TimeSpanPolyfill.cs b/src/Split/netcoreapp2.1/TimeSpanPolyfill.cs index 94d13cbbb..59d7c7974 100644 --- a/src/Split/netcoreapp2.1/TimeSpanPolyfill.cs +++ b/src/Split/netcoreapp2.1/TimeSpanPolyfill.cs @@ -15,7 +15,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microseconds => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new TimeSpan(target.Days, target.Hours, target.Minutes, 0); @@ -24,52 +24,71 @@ long TicksComponent() } } const long MicrosecondsToTicks = TimeSpan.TicksPerMillisecond / 1000; + const long MicrosecondsPerMillisecond = 1_000; + const long MicrosecondsPerSecond = 1_000_000; + const long MicrosecondsPerMinute = 60_000_000; + const long MicrosecondsPerHour = 3_600_000_000; + const long MicrosecondsPerDay = 86_400_000_000; + static TimeSpan TicksFromMicroseconds(decimal totalMicroseconds) + { + var ticks = totalMicroseconds * MicrosecondsToTicks; + if (ticks < long.MinValue || ticks > long.MaxValue) + { + throw new ArgumentOutOfRangeException(null, "TimeSpan overflowed because the duration is too long."); + } + return new((long)ticks); + } extension(TimeSpan) { /// /// Initializes a new instance of the structure to a specified number of days, hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromDays(int days, int hours = 0, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)days * TimeSpan.TicksPerDay + - (long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)days * MicrosecondsPerDay + + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromHours(int hours, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromMinutes(int minutes, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of seconds, milliseconds, and microseconds. /// public static TimeSpan FromSeconds(long seconds, long milliseconds = 0, long microseconds = 0) => - new(seconds * TimeSpan.TicksPerSecond + - milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of milliseconds and microseconds. /// public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0) => - new(milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of microseconds. /// public static TimeSpan FromMicroseconds(long microseconds) => - new(microseconds * MicrosecondsToTicks); + TicksFromMicroseconds(microseconds); } } diff --git a/src/Split/netcoreapp2.1/ZipFilePolyfill.cs b/src/Split/netcoreapp2.1/ZipFilePolyfill.cs index 6cb361e6f..d908c5633 100644 --- a/src/Split/netcoreapp2.1/ZipFilePolyfill.cs +++ b/src/Split/netcoreapp2.1/ZipFilePolyfill.cs @@ -53,11 +53,12 @@ static void ExtractToDirectoryPolyfill( string destinationDirectory, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + return; } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, null); } static void ExtractToDirectoryPolyfill( string sourceArchiveFile, @@ -65,11 +66,43 @@ static void ExtractToDirectoryPolyfill( Encoding encoding, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); + return; + } + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, encoding); + } + static void ExtractToDirectoryWithOverwrite( + string sourceArchiveFile, + string destinationDirectory, + Encoding? encoding) + { + var destinationRoot = Path.GetFullPath(destinationDirectory); + Directory.CreateDirectory(destinationRoot); + var normalizedRoot = destinationRoot; + if (normalizedRoot[normalizedRoot.Length - 1] != Path.DirectorySeparatorChar) + { + normalizedRoot += Path.DirectorySeparatorChar; + } + using var archive = encoding == null + ? ZipFile.OpenRead(sourceArchiveFile) + : ZipFile.Open(sourceArchiveFile, ZipArchiveMode.Read, encoding); + foreach (var entry in archive.Entries) + { + var destinationPath = Path.GetFullPath(Path.Combine(destinationRoot, entry.FullName)); + if (!destinationPath.StartsWith(normalizedRoot, StringComparison.Ordinal)) + { + throw new IOException("Extracting Zip entry would have resulted in a file outside the specified destination directory."); + } + if (entry.Name.Length == 0) + { + Directory.CreateDirectory(destinationPath); + continue; + } + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + entry.ExtractToFile(destinationPath, overwrite: true); } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); } /// /// Asynchronously opens a ZipArchive on the specified archiveFileName in the specified ZipArchiveMode mode. diff --git a/src/Split/netcoreapp2.2/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs b/src/Split/netcoreapp2.2/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs index 32b0afded..8bad1d83d 100644 --- a/src/Split/netcoreapp2.2/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs +++ b/src/Split/netcoreapp2.2/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs @@ -15,7 +15,7 @@ public static void ThrowIfZero(T value, [CallerArgumentExpression(nameof(valu { if (value.Equals(default)) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } /// Throws an if is zero. @@ -23,12 +23,12 @@ public static void ThrowIfZero(nint value, [CallerArgumentExpression(nameof(valu { if (value == 0) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } [DoesNotReturn] - static void ThrowZero(string? paramName) => - throw new ArgumentOutOfRangeException(paramName, "Value must not be zero."); + static void ThrowZero(T value, string? paramName) => + throw new ArgumentOutOfRangeException(paramName, value, $"{paramName} ('{value}') must be a non-zero value."); public static void ThrowIfNegative(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : struct, IComparable { @@ -112,7 +112,7 @@ public static void ThrowIfGreaterThanOrEqual(nint value, nint other, [CallerArgu { if (value >= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than {other}."); } } public static void ThrowIfLessThan(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) @@ -144,7 +144,7 @@ public static void ThrowIfLessThanOrEqual(nint value, nint other, [CallerArgumen { if (value <= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than {other}."); } } } diff --git a/src/Split/netcoreapp2.2/ConvertPolyfill.cs b/src/Split/netcoreapp2.2/ConvertPolyfill.cs index c7f6719c7..f3f76fb55 100644 --- a/src/Split/netcoreapp2.2/ConvertPolyfill.cs +++ b/src/Split/netcoreapp2.2/ConvertPolyfill.cs @@ -73,19 +73,19 @@ public static OperationStatus FromHexString(ReadOnlySpan source, Span= destination.Length) { charsConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal(source[i]); + var lo = GetHexVal(source[i + 1]); + if (hi < 0 || lo < 0) { - charsConsumed = i; + charsConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; @@ -132,19 +132,19 @@ public static OperationStatus FromHexString(ReadOnlySpan utf8Source, Span< var j = 0; while (i + 1 < utf8Source.Length) { - var hi = GetHexVal((char)utf8Source[i]); - var lo = GetHexVal((char)utf8Source[i + 1]); - if (hi < 0 || lo < 0) + if (j >= destination.Length) { bytesConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal((char)utf8Source[i]); + var lo = GetHexVal((char)utf8Source[i + 1]); + if (hi < 0 || lo < 0) { - bytesConsumed = i; + bytesConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; diff --git a/src/Split/netcoreapp2.2/DateTimeOffsetPolyfill.cs b/src/Split/netcoreapp2.2/DateTimeOffsetPolyfill.cs index 5a5018208..eb423b73a 100644 --- a/src/Split/netcoreapp2.2/DateTimeOffsetPolyfill.cs +++ b/src/Split/netcoreapp2.2/DateTimeOffsetPolyfill.cs @@ -17,7 +17,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTimeOffset(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Offset); diff --git a/src/Split/netcoreapp2.2/DateTimePolyfill.cs b/src/Split/netcoreapp2.2/DateTimePolyfill.cs index 4641f3e59..430683e14 100644 --- a/src/Split/netcoreapp2.2/DateTimePolyfill.cs +++ b/src/Split/netcoreapp2.2/DateTimePolyfill.cs @@ -16,7 +16,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTime(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Kind); diff --git a/src/Split/netcoreapp2.2/EqualityComparerPolyfill.cs b/src/Split/netcoreapp2.2/EqualityComparerPolyfill.cs index e45f2a30e..55cb3ad44 100644 --- a/src/Split/netcoreapp2.2/EqualityComparerPolyfill.cs +++ b/src/Split/netcoreapp2.2/EqualityComparerPolyfill.cs @@ -19,8 +19,14 @@ public override int GetHashCode(T obj) => /// public static EqualityComparer Create( Func equals, - Func? getHashCode = null) => - new DelegateEqualityComparer(equals, getHashCode); + Func? getHashCode = null) + { + if (equals is null) + { + throw new ArgumentNullException(nameof(equals)); + } + return new DelegateEqualityComparer(equals, getHashCode); + } } class KeySelectorEqualityComparer(Func keySelector, IEqualityComparer? keyComparer) : EqualityComparer @@ -41,7 +47,13 @@ public override int GetHashCode(T obj) /// public static EqualityComparer Create( Func keySelector, - IEqualityComparer? keyComparer = null) => - new KeySelectorEqualityComparer(keySelector, keyComparer); + IEqualityComparer? keyComparer = null) + { + if (keySelector is null) + { + throw new ArgumentNullException(nameof(keySelector)); + } + return new KeySelectorEqualityComparer(keySelector, keyComparer); + } } } diff --git a/src/Split/netcoreapp2.2/FileUnixModePolyfill.cs b/src/Split/netcoreapp2.2/FileUnixModePolyfill.cs index 2440f7800..701ef07cb 100644 --- a/src/Split/netcoreapp2.2/FileUnixModePolyfill.cs +++ b/src/Split/netcoreapp2.2/FileUnixModePolyfill.cs @@ -47,8 +47,8 @@ public static UnixFileMode GetUnixFileMode(string path) var mode = output[0] switch { '1' => UnixFileMode.StickyBit, - '2' => UnixFileMode.SetUser, - '4' => UnixFileMode.SetGroup, + '2' => UnixFileMode.SetGroup, + '4' => UnixFileMode.SetUser, _ => UnixFileMode.None }; mode = output[1] switch @@ -158,13 +158,13 @@ public static void SetUnixFileMode(string path, UnixFileMode mode) octal[3] += 1; break; case "setuser": - octal[0] += 2; + octal[0] += 4; break; case "stickybit": octal[0] += 1; break; case "setgroup": - octal[0] += 4; + octal[0] += 2; break; default: throw new("Invalid notation detected"); diff --git a/src/Split/netcoreapp2.2/Numbers/DoublePolyfill.cs b/src/Split/netcoreapp2.2/Numbers/DoublePolyfill.cs index ff9f8aca6..d08a8d3b6 100644 --- a/src/Split/netcoreapp2.2/Numbers/DoublePolyfill.cs +++ b/src/Split/netcoreapp2.2/Numbers/DoublePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out double result) => - double.TryParse(s, NumberStyles.Float, provider, out result); + double.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,12 +28,12 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its double-precision floating-point number equivalent.. /// public static bool TryParse(ReadOnlySpan utf8Text, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out double result) => - double.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + double.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/netcoreapp2.2/Numbers/SinglePolyfill.cs b/src/Split/netcoreapp2.2/Numbers/SinglePolyfill.cs index 3d574e6c7..6a49cbf6a 100644 --- a/src/Split/netcoreapp2.2/Numbers/SinglePolyfill.cs +++ b/src/Split/netcoreapp2.2/Numbers/SinglePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out float result) => - float.TryParse(s, NumberStyles.Float, provider, out result); + float.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,12 +28,12 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its single-precision floating-point number equivalent. /// public static bool TryParse(ReadOnlySpan utf8Text, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out float result) => - float.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + float.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/netcoreapp2.2/Polyfill.cs b/src/Split/netcoreapp2.2/Polyfill.cs index 16e916262..b7b21af17 100644 --- a/src/Split/netcoreapp2.2/Polyfill.cs +++ b/src/Split/netcoreapp2.2/Polyfill.cs @@ -17,5 +17,5 @@ namespace Polyfills; #endif static partial class Polyfill { - const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond * 1000; + const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; } diff --git a/src/Split/netcoreapp2.2/Polyfill_List.cs b/src/Split/netcoreapp2.2/Polyfill_List.cs index c2e76ed77..7a9348d0c 100644 --- a/src/Split/netcoreapp2.2/Polyfill_List.cs +++ b/src/Split/netcoreapp2.2/Polyfill_List.cs @@ -21,6 +21,10 @@ public static void AddRange(this List target, ReadOnlySpan source) /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { + if ((uint)index > (uint)target.Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } for (var i = 0; i < source.Length; i++) { var item = source[i]; @@ -30,6 +34,10 @@ public static void InsertRange(this List target, int index, ReadOnlySpanCopies the entire to a span. public static void CopyTo(this List target, Span destination) { + if (target.Count > destination.Length) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } for (var index = 0; index < target.Count; index++) { destination[index] = target[index]; diff --git a/src/Split/netcoreapp2.2/Polyfill_Memory_SpanSort.cs b/src/Split/netcoreapp2.2/Polyfill_Memory_SpanSort.cs index acc5c8666..b30957972 100644 --- a/src/Split/netcoreapp2.2/Polyfill_Memory_SpanSort.cs +++ b/src/Split/netcoreapp2.2/Polyfill_Memory_SpanSort.cs @@ -77,12 +77,8 @@ internal ComparerWrapper(Comparison comparison) throw new ArgumentNullException(nameof(comparison)); this.comparison = comparison; } - public int Compare(T? x, T? y) - { - if (x is null || y is null) - return 0; - return comparison((T)x, (T)y); - } + public int Compare(T? x, T? y) => + comparison(x!, y!); } } #endif diff --git a/src/Split/netcoreapp2.2/Polyfill_MicroNanosecondAdd.cs b/src/Split/netcoreapp2.2/Polyfill_MicroNanosecondAdd.cs index 17d2c8d00..e099e1260 100644 --- a/src/Split/netcoreapp2.2/Polyfill_MicroNanosecondAdd.cs +++ b/src/Split/netcoreapp2.2/Polyfill_MicroNanosecondAdd.cs @@ -8,10 +8,10 @@ static partial class Polyfill /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTime AddMicroseconds(this DateTime target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); /// /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTimeOffset AddMicroseconds(this DateTimeOffset target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); } diff --git a/src/Split/netcoreapp2.2/Polyfill_String.cs b/src/Split/netcoreapp2.2/Polyfill_String.cs index 2f9a508fd..bb7f8cfd4 100644 --- a/src/Split/netcoreapp2.2/Polyfill_String.cs +++ b/src/Split/netcoreapp2.2/Polyfill_String.cs @@ -3,8 +3,6 @@ namespace Polyfills; using System; using System.Text; -using System.IO; -using System.Runtime.CompilerServices; static partial class Polyfill { #if FeatureMemory @@ -66,22 +64,49 @@ public static int LastIndexOf(this string target, char value, int startIndex, in /// /// Replaces all newline sequences in the current string with . /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ReplaceLineEndings(this string target, string replacementText) { + var index = IndexOfNewlineChar(target, 0, out var stride); + if (index < 0) + { + return target; + } var builder = new StringBuilder(target.Length); - using var reader = new StringReader(target); + var position = 0; while (true) { - var line = reader.ReadLine(); - if (line == null) + builder.Append(target, position, index - position); + builder.Append(replacementText); + position = index + stride; + index = IndexOfNewlineChar(target, position, out stride); + if (index < 0) { break; } - builder.Append(line); - builder.Append(replacementText); } - return builder.ToString(0, builder.Length - replacementText.Length); + builder.Append(target, position, target.Length - position); + return builder.ToString(); + } + static int IndexOfNewlineChar(string text, int startIndex, out int stride) + { + stride = 0; + for (var index = startIndex; index < text.Length; index++) + { + switch (text[index]) + { + case '\r': + stride = index + 1 < text.Length && text[index + 1] == '\n' ? 2 : 1; + return index; + case '\n': + case '\f': + case '\u0085': + case '\u2028': + case '\u2029': + stride = 1; + return index; + } + } + return -1; } /// /// Replaces all newline sequences in the current string with . diff --git a/src/Split/netcoreapp2.2/RandomNumberGeneratorPolyfill.cs b/src/Split/netcoreapp2.2/RandomNumberGeneratorPolyfill.cs index c8b772fab..e90c042f9 100644 --- a/src/Split/netcoreapp2.2/RandomNumberGeneratorPolyfill.cs +++ b/src/Split/netcoreapp2.2/RandomNumberGeneratorPolyfill.cs @@ -34,7 +34,7 @@ public static int GetInt32(int fromInclusive, int toExclusive) { generator.GetBytes(bytes); value = BitConverter.ToUInt32(bytes, 0) & mask; - } while (value >= range); + } while (value > range); return (int) (fromInclusive + value); } /// diff --git a/src/Split/netcoreapp2.2/StringPolyfill.cs b/src/Split/netcoreapp2.2/StringPolyfill.cs index 6ae5df100..6412adfc4 100644 --- a/src/Split/netcoreapp2.2/StringPolyfill.cs +++ b/src/Split/netcoreapp2.2/StringPolyfill.cs @@ -50,6 +50,10 @@ public static string Join(char separator, scoped ReadOnlySpan values) span = span.Slice(1); } var value = values[index]; + if (value is null) + { + continue; + } value.CopyTo(span); span = span.Slice(value.Length); } diff --git a/src/Split/netcoreapp2.2/TaskCompletionSource.cs b/src/Split/netcoreapp2.2/TaskCompletionSource.cs index 04b507f76..67a35f938 100644 --- a/src/Split/netcoreapp2.2/TaskCompletionSource.cs +++ b/src/Split/netcoreapp2.2/TaskCompletionSource.cs @@ -77,5 +77,5 @@ public TaskCompletionSource(object? state, TaskCreationOptions creationOptions) /// /// Attempts to transition the underlying into the state. /// - public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(default); + public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(cancellationToken); } diff --git a/src/Split/netcoreapp2.2/TimeSpanPolyfill.cs b/src/Split/netcoreapp2.2/TimeSpanPolyfill.cs index 94d13cbbb..59d7c7974 100644 --- a/src/Split/netcoreapp2.2/TimeSpanPolyfill.cs +++ b/src/Split/netcoreapp2.2/TimeSpanPolyfill.cs @@ -15,7 +15,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microseconds => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new TimeSpan(target.Days, target.Hours, target.Minutes, 0); @@ -24,52 +24,71 @@ long TicksComponent() } } const long MicrosecondsToTicks = TimeSpan.TicksPerMillisecond / 1000; + const long MicrosecondsPerMillisecond = 1_000; + const long MicrosecondsPerSecond = 1_000_000; + const long MicrosecondsPerMinute = 60_000_000; + const long MicrosecondsPerHour = 3_600_000_000; + const long MicrosecondsPerDay = 86_400_000_000; + static TimeSpan TicksFromMicroseconds(decimal totalMicroseconds) + { + var ticks = totalMicroseconds * MicrosecondsToTicks; + if (ticks < long.MinValue || ticks > long.MaxValue) + { + throw new ArgumentOutOfRangeException(null, "TimeSpan overflowed because the duration is too long."); + } + return new((long)ticks); + } extension(TimeSpan) { /// /// Initializes a new instance of the structure to a specified number of days, hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromDays(int days, int hours = 0, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)days * TimeSpan.TicksPerDay + - (long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)days * MicrosecondsPerDay + + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromHours(int hours, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromMinutes(int minutes, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of seconds, milliseconds, and microseconds. /// public static TimeSpan FromSeconds(long seconds, long milliseconds = 0, long microseconds = 0) => - new(seconds * TimeSpan.TicksPerSecond + - milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of milliseconds and microseconds. /// public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0) => - new(milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of microseconds. /// public static TimeSpan FromMicroseconds(long microseconds) => - new(microseconds * MicrosecondsToTicks); + TicksFromMicroseconds(microseconds); } } diff --git a/src/Split/netcoreapp2.2/ZipFilePolyfill.cs b/src/Split/netcoreapp2.2/ZipFilePolyfill.cs index 6cb361e6f..d908c5633 100644 --- a/src/Split/netcoreapp2.2/ZipFilePolyfill.cs +++ b/src/Split/netcoreapp2.2/ZipFilePolyfill.cs @@ -53,11 +53,12 @@ static void ExtractToDirectoryPolyfill( string destinationDirectory, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + return; } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, null); } static void ExtractToDirectoryPolyfill( string sourceArchiveFile, @@ -65,11 +66,43 @@ static void ExtractToDirectoryPolyfill( Encoding encoding, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); + return; + } + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, encoding); + } + static void ExtractToDirectoryWithOverwrite( + string sourceArchiveFile, + string destinationDirectory, + Encoding? encoding) + { + var destinationRoot = Path.GetFullPath(destinationDirectory); + Directory.CreateDirectory(destinationRoot); + var normalizedRoot = destinationRoot; + if (normalizedRoot[normalizedRoot.Length - 1] != Path.DirectorySeparatorChar) + { + normalizedRoot += Path.DirectorySeparatorChar; + } + using var archive = encoding == null + ? ZipFile.OpenRead(sourceArchiveFile) + : ZipFile.Open(sourceArchiveFile, ZipArchiveMode.Read, encoding); + foreach (var entry in archive.Entries) + { + var destinationPath = Path.GetFullPath(Path.Combine(destinationRoot, entry.FullName)); + if (!destinationPath.StartsWith(normalizedRoot, StringComparison.Ordinal)) + { + throw new IOException("Extracting Zip entry would have resulted in a file outside the specified destination directory."); + } + if (entry.Name.Length == 0) + { + Directory.CreateDirectory(destinationPath); + continue; + } + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + entry.ExtractToFile(destinationPath, overwrite: true); } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); } /// /// Asynchronously opens a ZipArchive on the specified archiveFileName in the specified ZipArchiveMode mode. diff --git a/src/Split/netcoreapp3.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs b/src/Split/netcoreapp3.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs index 32b0afded..8bad1d83d 100644 --- a/src/Split/netcoreapp3.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs +++ b/src/Split/netcoreapp3.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs @@ -15,7 +15,7 @@ public static void ThrowIfZero(T value, [CallerArgumentExpression(nameof(valu { if (value.Equals(default)) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } /// Throws an if is zero. @@ -23,12 +23,12 @@ public static void ThrowIfZero(nint value, [CallerArgumentExpression(nameof(valu { if (value == 0) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } [DoesNotReturn] - static void ThrowZero(string? paramName) => - throw new ArgumentOutOfRangeException(paramName, "Value must not be zero."); + static void ThrowZero(T value, string? paramName) => + throw new ArgumentOutOfRangeException(paramName, value, $"{paramName} ('{value}') must be a non-zero value."); public static void ThrowIfNegative(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : struct, IComparable { @@ -112,7 +112,7 @@ public static void ThrowIfGreaterThanOrEqual(nint value, nint other, [CallerArgu { if (value >= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than {other}."); } } public static void ThrowIfLessThan(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) @@ -144,7 +144,7 @@ public static void ThrowIfLessThanOrEqual(nint value, nint other, [CallerArgumen { if (value <= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than {other}."); } } } diff --git a/src/Split/netcoreapp3.0/ConvertPolyfill.cs b/src/Split/netcoreapp3.0/ConvertPolyfill.cs index c7f6719c7..f3f76fb55 100644 --- a/src/Split/netcoreapp3.0/ConvertPolyfill.cs +++ b/src/Split/netcoreapp3.0/ConvertPolyfill.cs @@ -73,19 +73,19 @@ public static OperationStatus FromHexString(ReadOnlySpan source, Span= destination.Length) { charsConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal(source[i]); + var lo = GetHexVal(source[i + 1]); + if (hi < 0 || lo < 0) { - charsConsumed = i; + charsConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; @@ -132,19 +132,19 @@ public static OperationStatus FromHexString(ReadOnlySpan utf8Source, Span< var j = 0; while (i + 1 < utf8Source.Length) { - var hi = GetHexVal((char)utf8Source[i]); - var lo = GetHexVal((char)utf8Source[i + 1]); - if (hi < 0 || lo < 0) + if (j >= destination.Length) { bytesConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal((char)utf8Source[i]); + var lo = GetHexVal((char)utf8Source[i + 1]); + if (hi < 0 || lo < 0) { - bytesConsumed = i; + bytesConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; diff --git a/src/Split/netcoreapp3.0/DateTimeOffsetPolyfill.cs b/src/Split/netcoreapp3.0/DateTimeOffsetPolyfill.cs index 5a5018208..eb423b73a 100644 --- a/src/Split/netcoreapp3.0/DateTimeOffsetPolyfill.cs +++ b/src/Split/netcoreapp3.0/DateTimeOffsetPolyfill.cs @@ -17,7 +17,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTimeOffset(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Offset); diff --git a/src/Split/netcoreapp3.0/DateTimePolyfill.cs b/src/Split/netcoreapp3.0/DateTimePolyfill.cs index 4641f3e59..430683e14 100644 --- a/src/Split/netcoreapp3.0/DateTimePolyfill.cs +++ b/src/Split/netcoreapp3.0/DateTimePolyfill.cs @@ -16,7 +16,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTime(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Kind); diff --git a/src/Split/netcoreapp3.0/EqualityComparerPolyfill.cs b/src/Split/netcoreapp3.0/EqualityComparerPolyfill.cs index e45f2a30e..55cb3ad44 100644 --- a/src/Split/netcoreapp3.0/EqualityComparerPolyfill.cs +++ b/src/Split/netcoreapp3.0/EqualityComparerPolyfill.cs @@ -19,8 +19,14 @@ public override int GetHashCode(T obj) => /// public static EqualityComparer Create( Func equals, - Func? getHashCode = null) => - new DelegateEqualityComparer(equals, getHashCode); + Func? getHashCode = null) + { + if (equals is null) + { + throw new ArgumentNullException(nameof(equals)); + } + return new DelegateEqualityComparer(equals, getHashCode); + } } class KeySelectorEqualityComparer(Func keySelector, IEqualityComparer? keyComparer) : EqualityComparer @@ -41,7 +47,13 @@ public override int GetHashCode(T obj) /// public static EqualityComparer Create( Func keySelector, - IEqualityComparer? keyComparer = null) => - new KeySelectorEqualityComparer(keySelector, keyComparer); + IEqualityComparer? keyComparer = null) + { + if (keySelector is null) + { + throw new ArgumentNullException(nameof(keySelector)); + } + return new KeySelectorEqualityComparer(keySelector, keyComparer); + } } } diff --git a/src/Split/netcoreapp3.0/FileUnixModePolyfill.cs b/src/Split/netcoreapp3.0/FileUnixModePolyfill.cs index 2440f7800..701ef07cb 100644 --- a/src/Split/netcoreapp3.0/FileUnixModePolyfill.cs +++ b/src/Split/netcoreapp3.0/FileUnixModePolyfill.cs @@ -47,8 +47,8 @@ public static UnixFileMode GetUnixFileMode(string path) var mode = output[0] switch { '1' => UnixFileMode.StickyBit, - '2' => UnixFileMode.SetUser, - '4' => UnixFileMode.SetGroup, + '2' => UnixFileMode.SetGroup, + '4' => UnixFileMode.SetUser, _ => UnixFileMode.None }; mode = output[1] switch @@ -158,13 +158,13 @@ public static void SetUnixFileMode(string path, UnixFileMode mode) octal[3] += 1; break; case "setuser": - octal[0] += 2; + octal[0] += 4; break; case "stickybit": octal[0] += 1; break; case "setgroup": - octal[0] += 4; + octal[0] += 2; break; default: throw new("Invalid notation detected"); diff --git a/src/Split/netcoreapp3.0/Numbers/DoublePolyfill.cs b/src/Split/netcoreapp3.0/Numbers/DoublePolyfill.cs index ff9f8aca6..d08a8d3b6 100644 --- a/src/Split/netcoreapp3.0/Numbers/DoublePolyfill.cs +++ b/src/Split/netcoreapp3.0/Numbers/DoublePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out double result) => - double.TryParse(s, NumberStyles.Float, provider, out result); + double.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,12 +28,12 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its double-precision floating-point number equivalent.. /// public static bool TryParse(ReadOnlySpan utf8Text, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out double result) => - double.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + double.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/netcoreapp3.0/Numbers/SinglePolyfill.cs b/src/Split/netcoreapp3.0/Numbers/SinglePolyfill.cs index 3d574e6c7..6a49cbf6a 100644 --- a/src/Split/netcoreapp3.0/Numbers/SinglePolyfill.cs +++ b/src/Split/netcoreapp3.0/Numbers/SinglePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out float result) => - float.TryParse(s, NumberStyles.Float, provider, out result); + float.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,12 +28,12 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its single-precision floating-point number equivalent. /// public static bool TryParse(ReadOnlySpan utf8Text, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out float result) => - float.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + float.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/netcoreapp3.0/Polyfill.cs b/src/Split/netcoreapp3.0/Polyfill.cs index 16e916262..b7b21af17 100644 --- a/src/Split/netcoreapp3.0/Polyfill.cs +++ b/src/Split/netcoreapp3.0/Polyfill.cs @@ -17,5 +17,5 @@ namespace Polyfills; #endif static partial class Polyfill { - const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond * 1000; + const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; } diff --git a/src/Split/netcoreapp3.0/Polyfill_List.cs b/src/Split/netcoreapp3.0/Polyfill_List.cs index c2e76ed77..7a9348d0c 100644 --- a/src/Split/netcoreapp3.0/Polyfill_List.cs +++ b/src/Split/netcoreapp3.0/Polyfill_List.cs @@ -21,6 +21,10 @@ public static void AddRange(this List target, ReadOnlySpan source) /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { + if ((uint)index > (uint)target.Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } for (var i = 0; i < source.Length; i++) { var item = source[i]; @@ -30,6 +34,10 @@ public static void InsertRange(this List target, int index, ReadOnlySpanCopies the entire to a span. public static void CopyTo(this List target, Span destination) { + if (target.Count > destination.Length) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } for (var index = 0; index < target.Count; index++) { destination[index] = target[index]; diff --git a/src/Split/netcoreapp3.0/Polyfill_Memory_SpanSort.cs b/src/Split/netcoreapp3.0/Polyfill_Memory_SpanSort.cs index acc5c8666..b30957972 100644 --- a/src/Split/netcoreapp3.0/Polyfill_Memory_SpanSort.cs +++ b/src/Split/netcoreapp3.0/Polyfill_Memory_SpanSort.cs @@ -77,12 +77,8 @@ internal ComparerWrapper(Comparison comparison) throw new ArgumentNullException(nameof(comparison)); this.comparison = comparison; } - public int Compare(T? x, T? y) - { - if (x is null || y is null) - return 0; - return comparison((T)x, (T)y); - } + public int Compare(T? x, T? y) => + comparison(x!, y!); } } #endif diff --git a/src/Split/netcoreapp3.0/Polyfill_MicroNanosecondAdd.cs b/src/Split/netcoreapp3.0/Polyfill_MicroNanosecondAdd.cs index 17d2c8d00..e099e1260 100644 --- a/src/Split/netcoreapp3.0/Polyfill_MicroNanosecondAdd.cs +++ b/src/Split/netcoreapp3.0/Polyfill_MicroNanosecondAdd.cs @@ -8,10 +8,10 @@ static partial class Polyfill /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTime AddMicroseconds(this DateTime target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); /// /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTimeOffset AddMicroseconds(this DateTimeOffset target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); } diff --git a/src/Split/netcoreapp3.0/Polyfill_String.cs b/src/Split/netcoreapp3.0/Polyfill_String.cs index 2f9a508fd..bb7f8cfd4 100644 --- a/src/Split/netcoreapp3.0/Polyfill_String.cs +++ b/src/Split/netcoreapp3.0/Polyfill_String.cs @@ -3,8 +3,6 @@ namespace Polyfills; using System; using System.Text; -using System.IO; -using System.Runtime.CompilerServices; static partial class Polyfill { #if FeatureMemory @@ -66,22 +64,49 @@ public static int LastIndexOf(this string target, char value, int startIndex, in /// /// Replaces all newline sequences in the current string with . /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ReplaceLineEndings(this string target, string replacementText) { + var index = IndexOfNewlineChar(target, 0, out var stride); + if (index < 0) + { + return target; + } var builder = new StringBuilder(target.Length); - using var reader = new StringReader(target); + var position = 0; while (true) { - var line = reader.ReadLine(); - if (line == null) + builder.Append(target, position, index - position); + builder.Append(replacementText); + position = index + stride; + index = IndexOfNewlineChar(target, position, out stride); + if (index < 0) { break; } - builder.Append(line); - builder.Append(replacementText); } - return builder.ToString(0, builder.Length - replacementText.Length); + builder.Append(target, position, target.Length - position); + return builder.ToString(); + } + static int IndexOfNewlineChar(string text, int startIndex, out int stride) + { + stride = 0; + for (var index = startIndex; index < text.Length; index++) + { + switch (text[index]) + { + case '\r': + stride = index + 1 < text.Length && text[index + 1] == '\n' ? 2 : 1; + return index; + case '\n': + case '\f': + case '\u0085': + case '\u2028': + case '\u2029': + stride = 1; + return index; + } + } + return -1; } /// /// Replaces all newline sequences in the current string with . diff --git a/src/Split/netcoreapp3.0/StringPolyfill.cs b/src/Split/netcoreapp3.0/StringPolyfill.cs index 747f914bb..7d39ca017 100644 --- a/src/Split/netcoreapp3.0/StringPolyfill.cs +++ b/src/Split/netcoreapp3.0/StringPolyfill.cs @@ -50,6 +50,10 @@ public static string Join(char separator, scoped ReadOnlySpan values) span = span.Slice(1); } var value = values[index]; + if (value is null) + { + continue; + } value.CopyTo(span); span = span.Slice(value.Length); } diff --git a/src/Split/netcoreapp3.0/TaskCompletionSource.cs b/src/Split/netcoreapp3.0/TaskCompletionSource.cs index 04b507f76..67a35f938 100644 --- a/src/Split/netcoreapp3.0/TaskCompletionSource.cs +++ b/src/Split/netcoreapp3.0/TaskCompletionSource.cs @@ -77,5 +77,5 @@ public TaskCompletionSource(object? state, TaskCreationOptions creationOptions) /// /// Attempts to transition the underlying into the state. /// - public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(default); + public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(cancellationToken); } diff --git a/src/Split/netcoreapp3.0/TimeSpanPolyfill.cs b/src/Split/netcoreapp3.0/TimeSpanPolyfill.cs index 94d13cbbb..59d7c7974 100644 --- a/src/Split/netcoreapp3.0/TimeSpanPolyfill.cs +++ b/src/Split/netcoreapp3.0/TimeSpanPolyfill.cs @@ -15,7 +15,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microseconds => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new TimeSpan(target.Days, target.Hours, target.Minutes, 0); @@ -24,52 +24,71 @@ long TicksComponent() } } const long MicrosecondsToTicks = TimeSpan.TicksPerMillisecond / 1000; + const long MicrosecondsPerMillisecond = 1_000; + const long MicrosecondsPerSecond = 1_000_000; + const long MicrosecondsPerMinute = 60_000_000; + const long MicrosecondsPerHour = 3_600_000_000; + const long MicrosecondsPerDay = 86_400_000_000; + static TimeSpan TicksFromMicroseconds(decimal totalMicroseconds) + { + var ticks = totalMicroseconds * MicrosecondsToTicks; + if (ticks < long.MinValue || ticks > long.MaxValue) + { + throw new ArgumentOutOfRangeException(null, "TimeSpan overflowed because the duration is too long."); + } + return new((long)ticks); + } extension(TimeSpan) { /// /// Initializes a new instance of the structure to a specified number of days, hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromDays(int days, int hours = 0, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)days * TimeSpan.TicksPerDay + - (long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)days * MicrosecondsPerDay + + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromHours(int hours, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromMinutes(int minutes, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of seconds, milliseconds, and microseconds. /// public static TimeSpan FromSeconds(long seconds, long milliseconds = 0, long microseconds = 0) => - new(seconds * TimeSpan.TicksPerSecond + - milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of milliseconds and microseconds. /// public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0) => - new(milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of microseconds. /// public static TimeSpan FromMicroseconds(long microseconds) => - new(microseconds * MicrosecondsToTicks); + TicksFromMicroseconds(microseconds); } } diff --git a/src/Split/netcoreapp3.0/ZipFilePolyfill.cs b/src/Split/netcoreapp3.0/ZipFilePolyfill.cs index 6cb361e6f..d908c5633 100644 --- a/src/Split/netcoreapp3.0/ZipFilePolyfill.cs +++ b/src/Split/netcoreapp3.0/ZipFilePolyfill.cs @@ -53,11 +53,12 @@ static void ExtractToDirectoryPolyfill( string destinationDirectory, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + return; } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, null); } static void ExtractToDirectoryPolyfill( string sourceArchiveFile, @@ -65,11 +66,43 @@ static void ExtractToDirectoryPolyfill( Encoding encoding, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); + return; + } + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, encoding); + } + static void ExtractToDirectoryWithOverwrite( + string sourceArchiveFile, + string destinationDirectory, + Encoding? encoding) + { + var destinationRoot = Path.GetFullPath(destinationDirectory); + Directory.CreateDirectory(destinationRoot); + var normalizedRoot = destinationRoot; + if (normalizedRoot[normalizedRoot.Length - 1] != Path.DirectorySeparatorChar) + { + normalizedRoot += Path.DirectorySeparatorChar; + } + using var archive = encoding == null + ? ZipFile.OpenRead(sourceArchiveFile) + : ZipFile.Open(sourceArchiveFile, ZipArchiveMode.Read, encoding); + foreach (var entry in archive.Entries) + { + var destinationPath = Path.GetFullPath(Path.Combine(destinationRoot, entry.FullName)); + if (!destinationPath.StartsWith(normalizedRoot, StringComparison.Ordinal)) + { + throw new IOException("Extracting Zip entry would have resulted in a file outside the specified destination directory."); + } + if (entry.Name.Length == 0) + { + Directory.CreateDirectory(destinationPath); + continue; + } + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + entry.ExtractToFile(destinationPath, overwrite: true); } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); } /// /// Asynchronously opens a ZipArchive on the specified archiveFileName in the specified ZipArchiveMode mode. diff --git a/src/Split/netcoreapp3.1/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs b/src/Split/netcoreapp3.1/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs index 32b0afded..8bad1d83d 100644 --- a/src/Split/netcoreapp3.1/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs +++ b/src/Split/netcoreapp3.1/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs @@ -15,7 +15,7 @@ public static void ThrowIfZero(T value, [CallerArgumentExpression(nameof(valu { if (value.Equals(default)) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } /// Throws an if is zero. @@ -23,12 +23,12 @@ public static void ThrowIfZero(nint value, [CallerArgumentExpression(nameof(valu { if (value == 0) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } [DoesNotReturn] - static void ThrowZero(string? paramName) => - throw new ArgumentOutOfRangeException(paramName, "Value must not be zero."); + static void ThrowZero(T value, string? paramName) => + throw new ArgumentOutOfRangeException(paramName, value, $"{paramName} ('{value}') must be a non-zero value."); public static void ThrowIfNegative(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : struct, IComparable { @@ -112,7 +112,7 @@ public static void ThrowIfGreaterThanOrEqual(nint value, nint other, [CallerArgu { if (value >= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than {other}."); } } public static void ThrowIfLessThan(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) @@ -144,7 +144,7 @@ public static void ThrowIfLessThanOrEqual(nint value, nint other, [CallerArgumen { if (value <= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than {other}."); } } } diff --git a/src/Split/netcoreapp3.1/ConvertPolyfill.cs b/src/Split/netcoreapp3.1/ConvertPolyfill.cs index c7f6719c7..f3f76fb55 100644 --- a/src/Split/netcoreapp3.1/ConvertPolyfill.cs +++ b/src/Split/netcoreapp3.1/ConvertPolyfill.cs @@ -73,19 +73,19 @@ public static OperationStatus FromHexString(ReadOnlySpan source, Span= destination.Length) { charsConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal(source[i]); + var lo = GetHexVal(source[i + 1]); + if (hi < 0 || lo < 0) { - charsConsumed = i; + charsConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; @@ -132,19 +132,19 @@ public static OperationStatus FromHexString(ReadOnlySpan utf8Source, Span< var j = 0; while (i + 1 < utf8Source.Length) { - var hi = GetHexVal((char)utf8Source[i]); - var lo = GetHexVal((char)utf8Source[i + 1]); - if (hi < 0 || lo < 0) + if (j >= destination.Length) { bytesConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal((char)utf8Source[i]); + var lo = GetHexVal((char)utf8Source[i + 1]); + if (hi < 0 || lo < 0) { - bytesConsumed = i; + bytesConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; diff --git a/src/Split/netcoreapp3.1/DateTimeOffsetPolyfill.cs b/src/Split/netcoreapp3.1/DateTimeOffsetPolyfill.cs index 5a5018208..eb423b73a 100644 --- a/src/Split/netcoreapp3.1/DateTimeOffsetPolyfill.cs +++ b/src/Split/netcoreapp3.1/DateTimeOffsetPolyfill.cs @@ -17,7 +17,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTimeOffset(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Offset); diff --git a/src/Split/netcoreapp3.1/DateTimePolyfill.cs b/src/Split/netcoreapp3.1/DateTimePolyfill.cs index 4641f3e59..430683e14 100644 --- a/src/Split/netcoreapp3.1/DateTimePolyfill.cs +++ b/src/Split/netcoreapp3.1/DateTimePolyfill.cs @@ -16,7 +16,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTime(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Kind); diff --git a/src/Split/netcoreapp3.1/EqualityComparerPolyfill.cs b/src/Split/netcoreapp3.1/EqualityComparerPolyfill.cs index e45f2a30e..55cb3ad44 100644 --- a/src/Split/netcoreapp3.1/EqualityComparerPolyfill.cs +++ b/src/Split/netcoreapp3.1/EqualityComparerPolyfill.cs @@ -19,8 +19,14 @@ public override int GetHashCode(T obj) => /// public static EqualityComparer Create( Func equals, - Func? getHashCode = null) => - new DelegateEqualityComparer(equals, getHashCode); + Func? getHashCode = null) + { + if (equals is null) + { + throw new ArgumentNullException(nameof(equals)); + } + return new DelegateEqualityComparer(equals, getHashCode); + } } class KeySelectorEqualityComparer(Func keySelector, IEqualityComparer? keyComparer) : EqualityComparer @@ -41,7 +47,13 @@ public override int GetHashCode(T obj) /// public static EqualityComparer Create( Func keySelector, - IEqualityComparer? keyComparer = null) => - new KeySelectorEqualityComparer(keySelector, keyComparer); + IEqualityComparer? keyComparer = null) + { + if (keySelector is null) + { + throw new ArgumentNullException(nameof(keySelector)); + } + return new KeySelectorEqualityComparer(keySelector, keyComparer); + } } } diff --git a/src/Split/netcoreapp3.1/FileUnixModePolyfill.cs b/src/Split/netcoreapp3.1/FileUnixModePolyfill.cs index 2440f7800..701ef07cb 100644 --- a/src/Split/netcoreapp3.1/FileUnixModePolyfill.cs +++ b/src/Split/netcoreapp3.1/FileUnixModePolyfill.cs @@ -47,8 +47,8 @@ public static UnixFileMode GetUnixFileMode(string path) var mode = output[0] switch { '1' => UnixFileMode.StickyBit, - '2' => UnixFileMode.SetUser, - '4' => UnixFileMode.SetGroup, + '2' => UnixFileMode.SetGroup, + '4' => UnixFileMode.SetUser, _ => UnixFileMode.None }; mode = output[1] switch @@ -158,13 +158,13 @@ public static void SetUnixFileMode(string path, UnixFileMode mode) octal[3] += 1; break; case "setuser": - octal[0] += 2; + octal[0] += 4; break; case "stickybit": octal[0] += 1; break; case "setgroup": - octal[0] += 4; + octal[0] += 2; break; default: throw new("Invalid notation detected"); diff --git a/src/Split/netcoreapp3.1/Numbers/DoublePolyfill.cs b/src/Split/netcoreapp3.1/Numbers/DoublePolyfill.cs index ff9f8aca6..d08a8d3b6 100644 --- a/src/Split/netcoreapp3.1/Numbers/DoublePolyfill.cs +++ b/src/Split/netcoreapp3.1/Numbers/DoublePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out double result) => - double.TryParse(s, NumberStyles.Float, provider, out result); + double.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,12 +28,12 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its double-precision floating-point number equivalent.. /// public static bool TryParse(ReadOnlySpan utf8Text, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out double result) => - double.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + double.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/netcoreapp3.1/Numbers/SinglePolyfill.cs b/src/Split/netcoreapp3.1/Numbers/SinglePolyfill.cs index 3d574e6c7..6a49cbf6a 100644 --- a/src/Split/netcoreapp3.1/Numbers/SinglePolyfill.cs +++ b/src/Split/netcoreapp3.1/Numbers/SinglePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out float result) => - float.TryParse(s, NumberStyles.Float, provider, out result); + float.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,12 +28,12 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its single-precision floating-point number equivalent. /// public static bool TryParse(ReadOnlySpan utf8Text, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out float result) => - float.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + float.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/netcoreapp3.1/Polyfill.cs b/src/Split/netcoreapp3.1/Polyfill.cs index 16e916262..b7b21af17 100644 --- a/src/Split/netcoreapp3.1/Polyfill.cs +++ b/src/Split/netcoreapp3.1/Polyfill.cs @@ -17,5 +17,5 @@ namespace Polyfills; #endif static partial class Polyfill { - const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond * 1000; + const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; } diff --git a/src/Split/netcoreapp3.1/Polyfill_List.cs b/src/Split/netcoreapp3.1/Polyfill_List.cs index c2e76ed77..7a9348d0c 100644 --- a/src/Split/netcoreapp3.1/Polyfill_List.cs +++ b/src/Split/netcoreapp3.1/Polyfill_List.cs @@ -21,6 +21,10 @@ public static void AddRange(this List target, ReadOnlySpan source) /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { + if ((uint)index > (uint)target.Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } for (var i = 0; i < source.Length; i++) { var item = source[i]; @@ -30,6 +34,10 @@ public static void InsertRange(this List target, int index, ReadOnlySpanCopies the entire to a span. public static void CopyTo(this List target, Span destination) { + if (target.Count > destination.Length) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } for (var index = 0; index < target.Count; index++) { destination[index] = target[index]; diff --git a/src/Split/netcoreapp3.1/Polyfill_Memory_SpanSort.cs b/src/Split/netcoreapp3.1/Polyfill_Memory_SpanSort.cs index acc5c8666..b30957972 100644 --- a/src/Split/netcoreapp3.1/Polyfill_Memory_SpanSort.cs +++ b/src/Split/netcoreapp3.1/Polyfill_Memory_SpanSort.cs @@ -77,12 +77,8 @@ internal ComparerWrapper(Comparison comparison) throw new ArgumentNullException(nameof(comparison)); this.comparison = comparison; } - public int Compare(T? x, T? y) - { - if (x is null || y is null) - return 0; - return comparison((T)x, (T)y); - } + public int Compare(T? x, T? y) => + comparison(x!, y!); } } #endif diff --git a/src/Split/netcoreapp3.1/Polyfill_MicroNanosecondAdd.cs b/src/Split/netcoreapp3.1/Polyfill_MicroNanosecondAdd.cs index 17d2c8d00..e099e1260 100644 --- a/src/Split/netcoreapp3.1/Polyfill_MicroNanosecondAdd.cs +++ b/src/Split/netcoreapp3.1/Polyfill_MicroNanosecondAdd.cs @@ -8,10 +8,10 @@ static partial class Polyfill /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTime AddMicroseconds(this DateTime target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); /// /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTimeOffset AddMicroseconds(this DateTimeOffset target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); } diff --git a/src/Split/netcoreapp3.1/Polyfill_String.cs b/src/Split/netcoreapp3.1/Polyfill_String.cs index 2f9a508fd..bb7f8cfd4 100644 --- a/src/Split/netcoreapp3.1/Polyfill_String.cs +++ b/src/Split/netcoreapp3.1/Polyfill_String.cs @@ -3,8 +3,6 @@ namespace Polyfills; using System; using System.Text; -using System.IO; -using System.Runtime.CompilerServices; static partial class Polyfill { #if FeatureMemory @@ -66,22 +64,49 @@ public static int LastIndexOf(this string target, char value, int startIndex, in /// /// Replaces all newline sequences in the current string with . /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ReplaceLineEndings(this string target, string replacementText) { + var index = IndexOfNewlineChar(target, 0, out var stride); + if (index < 0) + { + return target; + } var builder = new StringBuilder(target.Length); - using var reader = new StringReader(target); + var position = 0; while (true) { - var line = reader.ReadLine(); - if (line == null) + builder.Append(target, position, index - position); + builder.Append(replacementText); + position = index + stride; + index = IndexOfNewlineChar(target, position, out stride); + if (index < 0) { break; } - builder.Append(line); - builder.Append(replacementText); } - return builder.ToString(0, builder.Length - replacementText.Length); + builder.Append(target, position, target.Length - position); + return builder.ToString(); + } + static int IndexOfNewlineChar(string text, int startIndex, out int stride) + { + stride = 0; + for (var index = startIndex; index < text.Length; index++) + { + switch (text[index]) + { + case '\r': + stride = index + 1 < text.Length && text[index + 1] == '\n' ? 2 : 1; + return index; + case '\n': + case '\f': + case '\u0085': + case '\u2028': + case '\u2029': + stride = 1; + return index; + } + } + return -1; } /// /// Replaces all newline sequences in the current string with . diff --git a/src/Split/netcoreapp3.1/StringPolyfill.cs b/src/Split/netcoreapp3.1/StringPolyfill.cs index 747f914bb..7d39ca017 100644 --- a/src/Split/netcoreapp3.1/StringPolyfill.cs +++ b/src/Split/netcoreapp3.1/StringPolyfill.cs @@ -50,6 +50,10 @@ public static string Join(char separator, scoped ReadOnlySpan values) span = span.Slice(1); } var value = values[index]; + if (value is null) + { + continue; + } value.CopyTo(span); span = span.Slice(value.Length); } diff --git a/src/Split/netcoreapp3.1/TaskCompletionSource.cs b/src/Split/netcoreapp3.1/TaskCompletionSource.cs index 04b507f76..67a35f938 100644 --- a/src/Split/netcoreapp3.1/TaskCompletionSource.cs +++ b/src/Split/netcoreapp3.1/TaskCompletionSource.cs @@ -77,5 +77,5 @@ public TaskCompletionSource(object? state, TaskCreationOptions creationOptions) /// /// Attempts to transition the underlying into the state. /// - public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(default); + public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(cancellationToken); } diff --git a/src/Split/netcoreapp3.1/TimeSpanPolyfill.cs b/src/Split/netcoreapp3.1/TimeSpanPolyfill.cs index 94d13cbbb..59d7c7974 100644 --- a/src/Split/netcoreapp3.1/TimeSpanPolyfill.cs +++ b/src/Split/netcoreapp3.1/TimeSpanPolyfill.cs @@ -15,7 +15,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microseconds => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new TimeSpan(target.Days, target.Hours, target.Minutes, 0); @@ -24,52 +24,71 @@ long TicksComponent() } } const long MicrosecondsToTicks = TimeSpan.TicksPerMillisecond / 1000; + const long MicrosecondsPerMillisecond = 1_000; + const long MicrosecondsPerSecond = 1_000_000; + const long MicrosecondsPerMinute = 60_000_000; + const long MicrosecondsPerHour = 3_600_000_000; + const long MicrosecondsPerDay = 86_400_000_000; + static TimeSpan TicksFromMicroseconds(decimal totalMicroseconds) + { + var ticks = totalMicroseconds * MicrosecondsToTicks; + if (ticks < long.MinValue || ticks > long.MaxValue) + { + throw new ArgumentOutOfRangeException(null, "TimeSpan overflowed because the duration is too long."); + } + return new((long)ticks); + } extension(TimeSpan) { /// /// Initializes a new instance of the structure to a specified number of days, hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromDays(int days, int hours = 0, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)days * TimeSpan.TicksPerDay + - (long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)days * MicrosecondsPerDay + + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromHours(int hours, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromMinutes(int minutes, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of seconds, milliseconds, and microseconds. /// public static TimeSpan FromSeconds(long seconds, long milliseconds = 0, long microseconds = 0) => - new(seconds * TimeSpan.TicksPerSecond + - milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of milliseconds and microseconds. /// public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0) => - new(milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of microseconds. /// public static TimeSpan FromMicroseconds(long microseconds) => - new(microseconds * MicrosecondsToTicks); + TicksFromMicroseconds(microseconds); } } diff --git a/src/Split/netcoreapp3.1/ZipFilePolyfill.cs b/src/Split/netcoreapp3.1/ZipFilePolyfill.cs index 6cb361e6f..d908c5633 100644 --- a/src/Split/netcoreapp3.1/ZipFilePolyfill.cs +++ b/src/Split/netcoreapp3.1/ZipFilePolyfill.cs @@ -53,11 +53,12 @@ static void ExtractToDirectoryPolyfill( string destinationDirectory, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + return; } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, null); } static void ExtractToDirectoryPolyfill( string sourceArchiveFile, @@ -65,11 +66,43 @@ static void ExtractToDirectoryPolyfill( Encoding encoding, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); + return; + } + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, encoding); + } + static void ExtractToDirectoryWithOverwrite( + string sourceArchiveFile, + string destinationDirectory, + Encoding? encoding) + { + var destinationRoot = Path.GetFullPath(destinationDirectory); + Directory.CreateDirectory(destinationRoot); + var normalizedRoot = destinationRoot; + if (normalizedRoot[normalizedRoot.Length - 1] != Path.DirectorySeparatorChar) + { + normalizedRoot += Path.DirectorySeparatorChar; + } + using var archive = encoding == null + ? ZipFile.OpenRead(sourceArchiveFile) + : ZipFile.Open(sourceArchiveFile, ZipArchiveMode.Read, encoding); + foreach (var entry in archive.Entries) + { + var destinationPath = Path.GetFullPath(Path.Combine(destinationRoot, entry.FullName)); + if (!destinationPath.StartsWith(normalizedRoot, StringComparison.Ordinal)) + { + throw new IOException("Extracting Zip entry would have resulted in a file outside the specified destination directory."); + } + if (entry.Name.Length == 0) + { + Directory.CreateDirectory(destinationPath); + continue; + } + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + entry.ExtractToFile(destinationPath, overwrite: true); } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); } /// /// Asynchronously opens a ZipArchive on the specified archiveFileName in the specified ZipArchiveMode mode. diff --git a/src/Split/netstandard2.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs b/src/Split/netstandard2.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs index 32b0afded..8bad1d83d 100644 --- a/src/Split/netstandard2.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs +++ b/src/Split/netstandard2.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs @@ -15,7 +15,7 @@ public static void ThrowIfZero(T value, [CallerArgumentExpression(nameof(valu { if (value.Equals(default)) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } /// Throws an if is zero. @@ -23,12 +23,12 @@ public static void ThrowIfZero(nint value, [CallerArgumentExpression(nameof(valu { if (value == 0) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } [DoesNotReturn] - static void ThrowZero(string? paramName) => - throw new ArgumentOutOfRangeException(paramName, "Value must not be zero."); + static void ThrowZero(T value, string? paramName) => + throw new ArgumentOutOfRangeException(paramName, value, $"{paramName} ('{value}') must be a non-zero value."); public static void ThrowIfNegative(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : struct, IComparable { @@ -112,7 +112,7 @@ public static void ThrowIfGreaterThanOrEqual(nint value, nint other, [CallerArgu { if (value >= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than {other}."); } } public static void ThrowIfLessThan(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) @@ -144,7 +144,7 @@ public static void ThrowIfLessThanOrEqual(nint value, nint other, [CallerArgumen { if (value <= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than {other}."); } } } diff --git a/src/Split/netstandard2.0/ConvertPolyfill.cs b/src/Split/netstandard2.0/ConvertPolyfill.cs index 412674710..1f45391f9 100644 --- a/src/Split/netstandard2.0/ConvertPolyfill.cs +++ b/src/Split/netstandard2.0/ConvertPolyfill.cs @@ -134,19 +134,19 @@ public static OperationStatus FromHexString(ReadOnlySpan source, Span= destination.Length) { charsConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal(source[i]); + var lo = GetHexVal(source[i + 1]); + if (hi < 0 || lo < 0) { - charsConsumed = i; + charsConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; @@ -193,19 +193,19 @@ public static OperationStatus FromHexString(ReadOnlySpan utf8Source, Span< var j = 0; while (i + 1 < utf8Source.Length) { - var hi = GetHexVal((char)utf8Source[i]); - var lo = GetHexVal((char)utf8Source[i + 1]); - if (hi < 0 || lo < 0) + if (j >= destination.Length) { bytesConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal((char)utf8Source[i]); + var lo = GetHexVal((char)utf8Source[i + 1]); + if (hi < 0 || lo < 0) { - bytesConsumed = i; + bytesConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; diff --git a/src/Split/netstandard2.0/DateTimeOffsetPolyfill.cs b/src/Split/netstandard2.0/DateTimeOffsetPolyfill.cs index 6837790e5..012b944dc 100644 --- a/src/Split/netstandard2.0/DateTimeOffsetPolyfill.cs +++ b/src/Split/netstandard2.0/DateTimeOffsetPolyfill.cs @@ -17,7 +17,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTimeOffset(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Offset); diff --git a/src/Split/netstandard2.0/DateTimePolyfill.cs b/src/Split/netstandard2.0/DateTimePolyfill.cs index 4641f3e59..430683e14 100644 --- a/src/Split/netstandard2.0/DateTimePolyfill.cs +++ b/src/Split/netstandard2.0/DateTimePolyfill.cs @@ -16,7 +16,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTime(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Kind); diff --git a/src/Split/netstandard2.0/EqualityComparerPolyfill.cs b/src/Split/netstandard2.0/EqualityComparerPolyfill.cs index e45f2a30e..55cb3ad44 100644 --- a/src/Split/netstandard2.0/EqualityComparerPolyfill.cs +++ b/src/Split/netstandard2.0/EqualityComparerPolyfill.cs @@ -19,8 +19,14 @@ public override int GetHashCode(T obj) => /// public static EqualityComparer Create( Func equals, - Func? getHashCode = null) => - new DelegateEqualityComparer(equals, getHashCode); + Func? getHashCode = null) + { + if (equals is null) + { + throw new ArgumentNullException(nameof(equals)); + } + return new DelegateEqualityComparer(equals, getHashCode); + } } class KeySelectorEqualityComparer(Func keySelector, IEqualityComparer? keyComparer) : EqualityComparer @@ -41,7 +47,13 @@ public override int GetHashCode(T obj) /// public static EqualityComparer Create( Func keySelector, - IEqualityComparer? keyComparer = null) => - new KeySelectorEqualityComparer(keySelector, keyComparer); + IEqualityComparer? keyComparer = null) + { + if (keySelector is null) + { + throw new ArgumentNullException(nameof(keySelector)); + } + return new KeySelectorEqualityComparer(keySelector, keyComparer); + } } } diff --git a/src/Split/netstandard2.0/FileUnixModePolyfill.cs b/src/Split/netstandard2.0/FileUnixModePolyfill.cs index 2440f7800..701ef07cb 100644 --- a/src/Split/netstandard2.0/FileUnixModePolyfill.cs +++ b/src/Split/netstandard2.0/FileUnixModePolyfill.cs @@ -47,8 +47,8 @@ public static UnixFileMode GetUnixFileMode(string path) var mode = output[0] switch { '1' => UnixFileMode.StickyBit, - '2' => UnixFileMode.SetUser, - '4' => UnixFileMode.SetGroup, + '2' => UnixFileMode.SetGroup, + '4' => UnixFileMode.SetUser, _ => UnixFileMode.None }; mode = output[1] switch @@ -158,13 +158,13 @@ public static void SetUnixFileMode(string path, UnixFileMode mode) octal[3] += 1; break; case "setuser": - octal[0] += 2; + octal[0] += 4; break; case "stickybit": octal[0] += 1; break; case "setgroup": - octal[0] += 4; + octal[0] += 2; break; default: throw new("Invalid notation detected"); diff --git a/src/Split/netstandard2.0/Numbers/DoublePolyfill.cs b/src/Split/netstandard2.0/Numbers/DoublePolyfill.cs index ba5333f69..2e3e40efd 100644 --- a/src/Split/netstandard2.0/Numbers/DoublePolyfill.cs +++ b/src/Split/netstandard2.0/Numbers/DoublePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out double result) => - double.TryParse(s, NumberStyles.Float, provider, out result); + double.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,7 +28,7 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its double-precision floating-point number equivalent.. /// public static bool TryParse(ReadOnlySpan utf8Text, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Converts the span representation of a number in a specified style and culture-specific format to its double-precision floating-point number equivalent. A return value indicates whether the conversion succeeded or failed. /// @@ -43,7 +43,7 @@ public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatPro /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out double result) => - double.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + double.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/netstandard2.0/Numbers/SinglePolyfill.cs b/src/Split/netstandard2.0/Numbers/SinglePolyfill.cs index 69af78095..2f6e02a82 100644 --- a/src/Split/netstandard2.0/Numbers/SinglePolyfill.cs +++ b/src/Split/netstandard2.0/Numbers/SinglePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out float result) => - float.TryParse(s, NumberStyles.Float, provider, out result); + float.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,7 +28,7 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its single-precision floating-point number equivalent. /// public static bool TryParse(ReadOnlySpan utf8Text, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Converts the span representation of a number in a specified style and culture-specific format to its single-precision floating-point number equivalent. A return value indicates whether the conversion succeeded or failed. /// @@ -43,7 +43,7 @@ public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatPro /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out float result) => - float.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + float.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/netstandard2.0/Polyfill.cs b/src/Split/netstandard2.0/Polyfill.cs index 16e916262..b7b21af17 100644 --- a/src/Split/netstandard2.0/Polyfill.cs +++ b/src/Split/netstandard2.0/Polyfill.cs @@ -17,5 +17,5 @@ namespace Polyfills; #endif static partial class Polyfill { - const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond * 1000; + const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; } diff --git a/src/Split/netstandard2.0/Polyfill_ArraySegment.cs b/src/Split/netstandard2.0/Polyfill_ArraySegment.cs index 96e83c18d..05ea1f659 100644 --- a/src/Split/netstandard2.0/Polyfill_ArraySegment.cs +++ b/src/Split/netstandard2.0/Polyfill_ArraySegment.cs @@ -12,15 +12,19 @@ static partial class Polyfill /// public static void CopyTo(this ArraySegment target, ArraySegment destination) { - if (target.Count > destination.Count) + if (target.Array == null) { - throw new ArgumentException("DestinationTooShort", nameof(destination)); + throw new InvalidOperationException("InvalidOperation_NullArray"); } - if (target.Array == null) + if (destination.Array == null) { throw new InvalidOperationException("InvalidOperation_NullArray"); } - Array.Copy(target.Array!, target.Offset, destination.Array, destination.Offset, target.Count); + if (target.Count > destination.Count) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } + Array.Copy(target.Array, target.Offset, destination.Array, destination.Offset, target.Count); } /// /// Copies the contents of this instance into the specified destination array of the same type T. diff --git a/src/Split/netstandard2.0/Polyfill_ConcurrentDictionary.cs b/src/Split/netstandard2.0/Polyfill_ConcurrentDictionary.cs index 8805de34b..a3547dd29 100644 --- a/src/Split/netstandard2.0/Polyfill_ConcurrentDictionary.cs +++ b/src/Split/netstandard2.0/Polyfill_ConcurrentDictionary.cs @@ -12,6 +12,10 @@ static partial class Polyfill public static TValue GetOrAdd(this ConcurrentDictionary target, TKey key, Func valueFactory, TArg factoryArgument) where TKey : notnull { + if (valueFactory == null) + { + throw new ArgumentNullException(nameof(valueFactory)); + } while (true) { TValue value; diff --git a/src/Split/netstandard2.0/Polyfill_Encoding_GetChars.cs b/src/Split/netstandard2.0/Polyfill_Encoding_GetChars.cs index f5a7de80b..4b956e845 100644 --- a/src/Split/netstandard2.0/Polyfill_Encoding_GetChars.cs +++ b/src/Split/netstandard2.0/Polyfill_Encoding_GetChars.cs @@ -25,7 +25,7 @@ public static int GetChars(this Encoding target, ReadOnlySpan bytes, Span< var charArray = new char[bytes.Length]; var array = bytes.ToArray(); var count = target.GetChars(array, 0, bytes.Length, charArray, 0); - new ReadOnlySpan(charArray).CopyTo(chars); + new ReadOnlySpan(charArray, 0, count).CopyTo(chars); return count; } #endif diff --git a/src/Split/netstandard2.0/Polyfill_List.cs b/src/Split/netstandard2.0/Polyfill_List.cs index c2e76ed77..7a9348d0c 100644 --- a/src/Split/netstandard2.0/Polyfill_List.cs +++ b/src/Split/netstandard2.0/Polyfill_List.cs @@ -21,6 +21,10 @@ public static void AddRange(this List target, ReadOnlySpan source) /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { + if ((uint)index > (uint)target.Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } for (var i = 0; i < source.Length; i++) { var item = source[i]; @@ -30,6 +34,10 @@ public static void InsertRange(this List target, int index, ReadOnlySpanCopies the entire to a span. public static void CopyTo(this List target, Span destination) { + if (target.Count > destination.Length) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } for (var index = 0; index < target.Count; index++) { destination[index] = target[index]; diff --git a/src/Split/netstandard2.0/Polyfill_Memory_SpanSort.cs b/src/Split/netstandard2.0/Polyfill_Memory_SpanSort.cs index acc5c8666..b30957972 100644 --- a/src/Split/netstandard2.0/Polyfill_Memory_SpanSort.cs +++ b/src/Split/netstandard2.0/Polyfill_Memory_SpanSort.cs @@ -77,12 +77,8 @@ internal ComparerWrapper(Comparison comparison) throw new ArgumentNullException(nameof(comparison)); this.comparison = comparison; } - public int Compare(T? x, T? y) - { - if (x is null || y is null) - return 0; - return comparison((T)x, (T)y); - } + public int Compare(T? x, T? y) => + comparison(x!, y!); } } #endif diff --git a/src/Split/netstandard2.0/Polyfill_MicroNanosecondAdd.cs b/src/Split/netstandard2.0/Polyfill_MicroNanosecondAdd.cs index 17d2c8d00..e099e1260 100644 --- a/src/Split/netstandard2.0/Polyfill_MicroNanosecondAdd.cs +++ b/src/Split/netstandard2.0/Polyfill_MicroNanosecondAdd.cs @@ -8,10 +8,10 @@ static partial class Polyfill /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTime AddMicroseconds(this DateTime target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); /// /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTimeOffset AddMicroseconds(this DateTimeOffset target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); } diff --git a/src/Split/netstandard2.0/Polyfill_String.cs b/src/Split/netstandard2.0/Polyfill_String.cs index bab55956e..254f4be0a 100644 --- a/src/Split/netstandard2.0/Polyfill_String.cs +++ b/src/Split/netstandard2.0/Polyfill_String.cs @@ -3,8 +3,6 @@ namespace Polyfills; using System; using System.Text; -using System.IO; -using System.Runtime.CompilerServices; static partial class Polyfill { #if FeatureMemory @@ -147,22 +145,49 @@ public static int LastIndexOf(this string target, char value, int startIndex, in /// /// Replaces all newline sequences in the current string with . /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ReplaceLineEndings(this string target, string replacementText) { + var index = IndexOfNewlineChar(target, 0, out var stride); + if (index < 0) + { + return target; + } var builder = new StringBuilder(target.Length); - using var reader = new StringReader(target); + var position = 0; while (true) { - var line = reader.ReadLine(); - if (line == null) + builder.Append(target, position, index - position); + builder.Append(replacementText); + position = index + stride; + index = IndexOfNewlineChar(target, position, out stride); + if (index < 0) { break; } - builder.Append(line); - builder.Append(replacementText); } - return builder.ToString(0, builder.Length - replacementText.Length); + builder.Append(target, position, target.Length - position); + return builder.ToString(); + } + static int IndexOfNewlineChar(string text, int startIndex, out int stride) + { + stride = 0; + for (var index = startIndex; index < text.Length; index++) + { + switch (text[index]) + { + case '\r': + stride = index + 1 < text.Length && text[index + 1] == '\n' ? 2 : 1; + return index; + case '\n': + case '\f': + case '\u0085': + case '\u2028': + case '\u2029': + stride = 1; + return index; + } + } + return -1; } /// /// Replaces all newline sequences in the current string with . diff --git a/src/Split/netstandard2.0/Polyfill_StringBuilder_Append.cs b/src/Split/netstandard2.0/Polyfill_StringBuilder_Append.cs index 0b60749a9..d3970ce53 100644 --- a/src/Split/netstandard2.0/Polyfill_StringBuilder_Append.cs +++ b/src/Split/netstandard2.0/Polyfill_StringBuilder_Append.cs @@ -10,6 +10,14 @@ static partial class Polyfill /// public static StringBuilder Append(this StringBuilder target, StringBuilder? value, int startIndex, int count) { + if (startIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(startIndex)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } if (value == null) { if (startIndex == 0 && count == 0) diff --git a/src/Split/netstandard2.0/Polyfill_StringBuilder_CopyTo.cs b/src/Split/netstandard2.0/Polyfill_StringBuilder_CopyTo.cs index dc625a7bc..ba99e0a1f 100644 --- a/src/Split/netstandard2.0/Polyfill_StringBuilder_CopyTo.cs +++ b/src/Split/netstandard2.0/Polyfill_StringBuilder_CopyTo.cs @@ -15,20 +15,25 @@ public static void CopyTo( Span destination, int count) { - var destinationIndex = 0; - while (true) + if (count < 0) { - if (sourceIndex == target.Length) - { - break; - } - if (destinationIndex == count) - { - break; - } - destination[destinationIndex] = target[sourceIndex]; - destinationIndex++; - sourceIndex++; + throw new ArgumentOutOfRangeException(nameof(count)); + } + if ((uint)sourceIndex > (uint)target.Length) + { + throw new ArgumentOutOfRangeException(nameof(sourceIndex)); + } + if (sourceIndex > target.Length - count) + { + throw new ArgumentException("The source StringBuilder does not contain enough characters from sourceIndex onward to satisfy count."); + } + if (count > destination.Length) + { + throw new ArgumentException("The destination span is too short to hold the requested characters.", nameof(destination)); + } + for (var index = 0; index < count; index++) + { + destination[index] = target[sourceIndex + index]; } } } diff --git a/src/Split/netstandard2.0/Polyfill_Type.cs b/src/Split/netstandard2.0/Polyfill_Type.cs index 7890e2168..b99bdfab5 100644 --- a/src/Split/netstandard2.0/Polyfill_Type.cs +++ b/src/Split/netstandard2.0/Polyfill_Type.cs @@ -7,9 +7,15 @@ namespace Polyfills; using System.Linq; static partial class Polyfill { - public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInfo other) => - target.MetadataToken == other.MetadataToken && - target.Module.Equals(other.Module); + public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInfo other) + { + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } + return target.MetadataToken == other.MetadataToken && + target.Module.Equals(other.Module); + } /// /// Searches for the specified method whose parameters match the specified generic parameter count, argument types and modifiers, using the specified binding constraints. /// @@ -22,6 +28,25 @@ public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInf Type[] types, ParameterModifier[]? modifiers) { + if (name is null) + { + throw new ArgumentNullException(nameof(name)); + } + if (genericParameterCount < 0) + { + throw new ArgumentOutOfRangeException(nameof(genericParameterCount)); + } + if (types is null) + { + throw new ArgumentNullException(nameof(types)); + } + foreach (var type in types) + { + if (type is null) + { + throw new ArgumentNullException(nameof(types)); + } + } var methods = target.GetMethods(bindingAttr); if (genericParameterCount == 0) { diff --git a/src/Split/netstandard2.0/RandomNumberGeneratorPolyfill.cs b/src/Split/netstandard2.0/RandomNumberGeneratorPolyfill.cs index 08384a7ea..539aa7aba 100644 --- a/src/Split/netstandard2.0/RandomNumberGeneratorPolyfill.cs +++ b/src/Split/netstandard2.0/RandomNumberGeneratorPolyfill.cs @@ -34,7 +34,7 @@ public static int GetInt32(int fromInclusive, int toExclusive) { generator.GetBytes(bytes); value = BitConverter.ToUInt32(bytes, 0) & mask; - } while (value >= range); + } while (value > range); return (int) (fromInclusive + value); } /// diff --git a/src/Split/netstandard2.0/StringPolyfill.cs b/src/Split/netstandard2.0/StringPolyfill.cs index bbdee2733..b14512f1d 100644 --- a/src/Split/netstandard2.0/StringPolyfill.cs +++ b/src/Split/netstandard2.0/StringPolyfill.cs @@ -50,6 +50,10 @@ public static string Join(char separator, scoped ReadOnlySpan values) span = span.Slice(1); } var value = values[index]; + if (value is null) + { + continue; + } value.CopyTo(span); span = span.Slice(value.Length); } diff --git a/src/Split/netstandard2.0/TaskCompletionSource.cs b/src/Split/netstandard2.0/TaskCompletionSource.cs index 04b507f76..67a35f938 100644 --- a/src/Split/netstandard2.0/TaskCompletionSource.cs +++ b/src/Split/netstandard2.0/TaskCompletionSource.cs @@ -77,5 +77,5 @@ public TaskCompletionSource(object? state, TaskCreationOptions creationOptions) /// /// Attempts to transition the underlying into the state. /// - public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(default); + public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(cancellationToken); } diff --git a/src/Split/netstandard2.0/TimeSpanPolyfill.cs b/src/Split/netstandard2.0/TimeSpanPolyfill.cs index 94d13cbbb..59d7c7974 100644 --- a/src/Split/netstandard2.0/TimeSpanPolyfill.cs +++ b/src/Split/netstandard2.0/TimeSpanPolyfill.cs @@ -15,7 +15,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microseconds => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new TimeSpan(target.Days, target.Hours, target.Minutes, 0); @@ -24,52 +24,71 @@ long TicksComponent() } } const long MicrosecondsToTicks = TimeSpan.TicksPerMillisecond / 1000; + const long MicrosecondsPerMillisecond = 1_000; + const long MicrosecondsPerSecond = 1_000_000; + const long MicrosecondsPerMinute = 60_000_000; + const long MicrosecondsPerHour = 3_600_000_000; + const long MicrosecondsPerDay = 86_400_000_000; + static TimeSpan TicksFromMicroseconds(decimal totalMicroseconds) + { + var ticks = totalMicroseconds * MicrosecondsToTicks; + if (ticks < long.MinValue || ticks > long.MaxValue) + { + throw new ArgumentOutOfRangeException(null, "TimeSpan overflowed because the duration is too long."); + } + return new((long)ticks); + } extension(TimeSpan) { /// /// Initializes a new instance of the structure to a specified number of days, hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromDays(int days, int hours = 0, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)days * TimeSpan.TicksPerDay + - (long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)days * MicrosecondsPerDay + + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromHours(int hours, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromMinutes(int minutes, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of seconds, milliseconds, and microseconds. /// public static TimeSpan FromSeconds(long seconds, long milliseconds = 0, long microseconds = 0) => - new(seconds * TimeSpan.TicksPerSecond + - milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of milliseconds and microseconds. /// public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0) => - new(milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of microseconds. /// public static TimeSpan FromMicroseconds(long microseconds) => - new(microseconds * MicrosecondsToTicks); + TicksFromMicroseconds(microseconds); } } diff --git a/src/Split/netstandard2.0/XDocumentPolyfill.cs b/src/Split/netstandard2.0/XDocumentPolyfill.cs index 1611acf19..876842223 100644 --- a/src/Split/netstandard2.0/XDocumentPolyfill.cs +++ b/src/Split/netstandard2.0/XDocumentPolyfill.cs @@ -25,12 +25,10 @@ static partial class XDocumentPolyfill /// /// Asynchronously creates a new XDocument and initializes its underlying XML tree using the specified stream, optionally preserving white space. /// - public static async Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) + public static Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - using var reader = new StreamReader(stream); - var content = await reader.ReadToEndAsync(); - return XDocument.Parse(content, options); + return Task.FromResult(XDocument.Load(stream, options)); } /// /// Asynchronously creates a new XDocument and initializes its underlying XML tree using the specified text reader, optionally preserving white space. diff --git a/src/Split/netstandard2.0/XElementPolyfill.cs b/src/Split/netstandard2.0/XElementPolyfill.cs index 27208879a..9347d9cdd 100644 --- a/src/Split/netstandard2.0/XElementPolyfill.cs +++ b/src/Split/netstandard2.0/XElementPolyfill.cs @@ -25,12 +25,10 @@ static partial class XElementPolyfill /// /// Asynchronously creates a new XElement and initializes its underlying XML tree using the specified stream, optionally preserving white space. /// - public static async Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) + public static Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - using var reader = new StreamReader(stream); - var content = await reader.ReadToEndAsync(); - return XElement.Parse(content, options); + return Task.FromResult(XElement.Load(stream, options)); } /// /// Asynchronously creates a new XElement and initializes its underlying XML tree using the specified text reader, optionally preserving white space. diff --git a/src/Split/netstandard2.0/ZipFilePolyfill.cs b/src/Split/netstandard2.0/ZipFilePolyfill.cs index 6cb361e6f..d908c5633 100644 --- a/src/Split/netstandard2.0/ZipFilePolyfill.cs +++ b/src/Split/netstandard2.0/ZipFilePolyfill.cs @@ -53,11 +53,12 @@ static void ExtractToDirectoryPolyfill( string destinationDirectory, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + return; } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, null); } static void ExtractToDirectoryPolyfill( string sourceArchiveFile, @@ -65,11 +66,43 @@ static void ExtractToDirectoryPolyfill( Encoding encoding, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); + return; + } + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, encoding); + } + static void ExtractToDirectoryWithOverwrite( + string sourceArchiveFile, + string destinationDirectory, + Encoding? encoding) + { + var destinationRoot = Path.GetFullPath(destinationDirectory); + Directory.CreateDirectory(destinationRoot); + var normalizedRoot = destinationRoot; + if (normalizedRoot[normalizedRoot.Length - 1] != Path.DirectorySeparatorChar) + { + normalizedRoot += Path.DirectorySeparatorChar; + } + using var archive = encoding == null + ? ZipFile.OpenRead(sourceArchiveFile) + : ZipFile.Open(sourceArchiveFile, ZipArchiveMode.Read, encoding); + foreach (var entry in archive.Entries) + { + var destinationPath = Path.GetFullPath(Path.Combine(destinationRoot, entry.FullName)); + if (!destinationPath.StartsWith(normalizedRoot, StringComparison.Ordinal)) + { + throw new IOException("Extracting Zip entry would have resulted in a file outside the specified destination directory."); + } + if (entry.Name.Length == 0) + { + Directory.CreateDirectory(destinationPath); + continue; + } + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + entry.ExtractToFile(destinationPath, overwrite: true); } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); } /// /// Asynchronously opens a ZipArchive on the specified archiveFileName in the specified ZipArchiveMode mode. diff --git a/src/Split/netstandard2.1/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs b/src/Split/netstandard2.1/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs index 32b0afded..8bad1d83d 100644 --- a/src/Split/netstandard2.1/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs +++ b/src/Split/netstandard2.1/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs @@ -15,7 +15,7 @@ public static void ThrowIfZero(T value, [CallerArgumentExpression(nameof(valu { if (value.Equals(default)) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } /// Throws an if is zero. @@ -23,12 +23,12 @@ public static void ThrowIfZero(nint value, [CallerArgumentExpression(nameof(valu { if (value == 0) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } [DoesNotReturn] - static void ThrowZero(string? paramName) => - throw new ArgumentOutOfRangeException(paramName, "Value must not be zero."); + static void ThrowZero(T value, string? paramName) => + throw new ArgumentOutOfRangeException(paramName, value, $"{paramName} ('{value}') must be a non-zero value."); public static void ThrowIfNegative(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : struct, IComparable { @@ -112,7 +112,7 @@ public static void ThrowIfGreaterThanOrEqual(nint value, nint other, [CallerArgu { if (value >= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than {other}."); } } public static void ThrowIfLessThan(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) @@ -144,7 +144,7 @@ public static void ThrowIfLessThanOrEqual(nint value, nint other, [CallerArgumen { if (value <= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than {other}."); } } } diff --git a/src/Split/netstandard2.1/ConvertPolyfill.cs b/src/Split/netstandard2.1/ConvertPolyfill.cs index c7f6719c7..f3f76fb55 100644 --- a/src/Split/netstandard2.1/ConvertPolyfill.cs +++ b/src/Split/netstandard2.1/ConvertPolyfill.cs @@ -73,19 +73,19 @@ public static OperationStatus FromHexString(ReadOnlySpan source, Span= destination.Length) { charsConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal(source[i]); + var lo = GetHexVal(source[i + 1]); + if (hi < 0 || lo < 0) { - charsConsumed = i; + charsConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; @@ -132,19 +132,19 @@ public static OperationStatus FromHexString(ReadOnlySpan utf8Source, Span< var j = 0; while (i + 1 < utf8Source.Length) { - var hi = GetHexVal((char)utf8Source[i]); - var lo = GetHexVal((char)utf8Source[i + 1]); - if (hi < 0 || lo < 0) + if (j >= destination.Length) { bytesConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal((char)utf8Source[i]); + var lo = GetHexVal((char)utf8Source[i + 1]); + if (hi < 0 || lo < 0) { - bytesConsumed = i; + bytesConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; diff --git a/src/Split/netstandard2.1/DateTimeOffsetPolyfill.cs b/src/Split/netstandard2.1/DateTimeOffsetPolyfill.cs index 15ba54f72..f0cf6a209 100644 --- a/src/Split/netstandard2.1/DateTimeOffsetPolyfill.cs +++ b/src/Split/netstandard2.1/DateTimeOffsetPolyfill.cs @@ -17,7 +17,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTimeOffset(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Offset); diff --git a/src/Split/netstandard2.1/DateTimePolyfill.cs b/src/Split/netstandard2.1/DateTimePolyfill.cs index 8eb8c705b..faa990de5 100644 --- a/src/Split/netstandard2.1/DateTimePolyfill.cs +++ b/src/Split/netstandard2.1/DateTimePolyfill.cs @@ -16,7 +16,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTime(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Kind); diff --git a/src/Split/netstandard2.1/EqualityComparerPolyfill.cs b/src/Split/netstandard2.1/EqualityComparerPolyfill.cs index e45f2a30e..55cb3ad44 100644 --- a/src/Split/netstandard2.1/EqualityComparerPolyfill.cs +++ b/src/Split/netstandard2.1/EqualityComparerPolyfill.cs @@ -19,8 +19,14 @@ public override int GetHashCode(T obj) => /// public static EqualityComparer Create( Func equals, - Func? getHashCode = null) => - new DelegateEqualityComparer(equals, getHashCode); + Func? getHashCode = null) + { + if (equals is null) + { + throw new ArgumentNullException(nameof(equals)); + } + return new DelegateEqualityComparer(equals, getHashCode); + } } class KeySelectorEqualityComparer(Func keySelector, IEqualityComparer? keyComparer) : EqualityComparer @@ -41,7 +47,13 @@ public override int GetHashCode(T obj) /// public static EqualityComparer Create( Func keySelector, - IEqualityComparer? keyComparer = null) => - new KeySelectorEqualityComparer(keySelector, keyComparer); + IEqualityComparer? keyComparer = null) + { + if (keySelector is null) + { + throw new ArgumentNullException(nameof(keySelector)); + } + return new KeySelectorEqualityComparer(keySelector, keyComparer); + } } } diff --git a/src/Split/netstandard2.1/FileUnixModePolyfill.cs b/src/Split/netstandard2.1/FileUnixModePolyfill.cs index 2440f7800..701ef07cb 100644 --- a/src/Split/netstandard2.1/FileUnixModePolyfill.cs +++ b/src/Split/netstandard2.1/FileUnixModePolyfill.cs @@ -47,8 +47,8 @@ public static UnixFileMode GetUnixFileMode(string path) var mode = output[0] switch { '1' => UnixFileMode.StickyBit, - '2' => UnixFileMode.SetUser, - '4' => UnixFileMode.SetGroup, + '2' => UnixFileMode.SetGroup, + '4' => UnixFileMode.SetUser, _ => UnixFileMode.None }; mode = output[1] switch @@ -158,13 +158,13 @@ public static void SetUnixFileMode(string path, UnixFileMode mode) octal[3] += 1; break; case "setuser": - octal[0] += 2; + octal[0] += 4; break; case "stickybit": octal[0] += 1; break; case "setgroup": - octal[0] += 4; + octal[0] += 2; break; default: throw new("Invalid notation detected"); diff --git a/src/Split/netstandard2.1/Numbers/DoublePolyfill.cs b/src/Split/netstandard2.1/Numbers/DoublePolyfill.cs index ff9f8aca6..d08a8d3b6 100644 --- a/src/Split/netstandard2.1/Numbers/DoublePolyfill.cs +++ b/src/Split/netstandard2.1/Numbers/DoublePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out double result) => - double.TryParse(s, NumberStyles.Float, provider, out result); + double.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,12 +28,12 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its double-precision floating-point number equivalent.. /// public static bool TryParse(ReadOnlySpan utf8Text, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out double result) => - double.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + double.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/netstandard2.1/Numbers/SinglePolyfill.cs b/src/Split/netstandard2.1/Numbers/SinglePolyfill.cs index 3d574e6c7..6a49cbf6a 100644 --- a/src/Split/netstandard2.1/Numbers/SinglePolyfill.cs +++ b/src/Split/netstandard2.1/Numbers/SinglePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out float result) => - float.TryParse(s, NumberStyles.Float, provider, out result); + float.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,12 +28,12 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its single-precision floating-point number equivalent. /// public static bool TryParse(ReadOnlySpan utf8Text, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out float result) => - float.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + float.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/netstandard2.1/Polyfill.cs b/src/Split/netstandard2.1/Polyfill.cs index 16e916262..b7b21af17 100644 --- a/src/Split/netstandard2.1/Polyfill.cs +++ b/src/Split/netstandard2.1/Polyfill.cs @@ -17,5 +17,5 @@ namespace Polyfills; #endif static partial class Polyfill { - const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond * 1000; + const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; } diff --git a/src/Split/netstandard2.1/Polyfill_List.cs b/src/Split/netstandard2.1/Polyfill_List.cs index c2e76ed77..7a9348d0c 100644 --- a/src/Split/netstandard2.1/Polyfill_List.cs +++ b/src/Split/netstandard2.1/Polyfill_List.cs @@ -21,6 +21,10 @@ public static void AddRange(this List target, ReadOnlySpan source) /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { + if ((uint)index > (uint)target.Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } for (var i = 0; i < source.Length; i++) { var item = source[i]; @@ -30,6 +34,10 @@ public static void InsertRange(this List target, int index, ReadOnlySpanCopies the entire to a span. public static void CopyTo(this List target, Span destination) { + if (target.Count > destination.Length) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } for (var index = 0; index < target.Count; index++) { destination[index] = target[index]; diff --git a/src/Split/netstandard2.1/Polyfill_Memory_SpanSort.cs b/src/Split/netstandard2.1/Polyfill_Memory_SpanSort.cs index acc5c8666..b30957972 100644 --- a/src/Split/netstandard2.1/Polyfill_Memory_SpanSort.cs +++ b/src/Split/netstandard2.1/Polyfill_Memory_SpanSort.cs @@ -77,12 +77,8 @@ internal ComparerWrapper(Comparison comparison) throw new ArgumentNullException(nameof(comparison)); this.comparison = comparison; } - public int Compare(T? x, T? y) - { - if (x is null || y is null) - return 0; - return comparison((T)x, (T)y); - } + public int Compare(T? x, T? y) => + comparison(x!, y!); } } #endif diff --git a/src/Split/netstandard2.1/Polyfill_MicroNanosecondAdd.cs b/src/Split/netstandard2.1/Polyfill_MicroNanosecondAdd.cs index 17d2c8d00..e099e1260 100644 --- a/src/Split/netstandard2.1/Polyfill_MicroNanosecondAdd.cs +++ b/src/Split/netstandard2.1/Polyfill_MicroNanosecondAdd.cs @@ -8,10 +8,10 @@ static partial class Polyfill /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTime AddMicroseconds(this DateTime target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); /// /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTimeOffset AddMicroseconds(this DateTimeOffset target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); } diff --git a/src/Split/netstandard2.1/Polyfill_String.cs b/src/Split/netstandard2.1/Polyfill_String.cs index 2f9a508fd..bb7f8cfd4 100644 --- a/src/Split/netstandard2.1/Polyfill_String.cs +++ b/src/Split/netstandard2.1/Polyfill_String.cs @@ -3,8 +3,6 @@ namespace Polyfills; using System; using System.Text; -using System.IO; -using System.Runtime.CompilerServices; static partial class Polyfill { #if FeatureMemory @@ -66,22 +64,49 @@ public static int LastIndexOf(this string target, char value, int startIndex, in /// /// Replaces all newline sequences in the current string with . /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ReplaceLineEndings(this string target, string replacementText) { + var index = IndexOfNewlineChar(target, 0, out var stride); + if (index < 0) + { + return target; + } var builder = new StringBuilder(target.Length); - using var reader = new StringReader(target); + var position = 0; while (true) { - var line = reader.ReadLine(); - if (line == null) + builder.Append(target, position, index - position); + builder.Append(replacementText); + position = index + stride; + index = IndexOfNewlineChar(target, position, out stride); + if (index < 0) { break; } - builder.Append(line); - builder.Append(replacementText); } - return builder.ToString(0, builder.Length - replacementText.Length); + builder.Append(target, position, target.Length - position); + return builder.ToString(); + } + static int IndexOfNewlineChar(string text, int startIndex, out int stride) + { + stride = 0; + for (var index = startIndex; index < text.Length; index++) + { + switch (text[index]) + { + case '\r': + stride = index + 1 < text.Length && text[index + 1] == '\n' ? 2 : 1; + return index; + case '\n': + case '\f': + case '\u0085': + case '\u2028': + case '\u2029': + stride = 1; + return index; + } + } + return -1; } /// /// Replaces all newline sequences in the current string with . diff --git a/src/Split/netstandard2.1/StringPolyfill.cs b/src/Split/netstandard2.1/StringPolyfill.cs index eb03e5e0b..711571a5e 100644 --- a/src/Split/netstandard2.1/StringPolyfill.cs +++ b/src/Split/netstandard2.1/StringPolyfill.cs @@ -50,6 +50,10 @@ public static string Join(char separator, scoped ReadOnlySpan values) span = span.Slice(1); } var value = values[index]; + if (value is null) + { + continue; + } value.CopyTo(span); span = span.Slice(value.Length); } diff --git a/src/Split/netstandard2.1/TaskCompletionSource.cs b/src/Split/netstandard2.1/TaskCompletionSource.cs index 04b507f76..67a35f938 100644 --- a/src/Split/netstandard2.1/TaskCompletionSource.cs +++ b/src/Split/netstandard2.1/TaskCompletionSource.cs @@ -77,5 +77,5 @@ public TaskCompletionSource(object? state, TaskCreationOptions creationOptions) /// /// Attempts to transition the underlying into the state. /// - public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(default); + public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(cancellationToken); } diff --git a/src/Split/netstandard2.1/TimeSpanPolyfill.cs b/src/Split/netstandard2.1/TimeSpanPolyfill.cs index 94d13cbbb..59d7c7974 100644 --- a/src/Split/netstandard2.1/TimeSpanPolyfill.cs +++ b/src/Split/netstandard2.1/TimeSpanPolyfill.cs @@ -15,7 +15,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microseconds => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new TimeSpan(target.Days, target.Hours, target.Minutes, 0); @@ -24,52 +24,71 @@ long TicksComponent() } } const long MicrosecondsToTicks = TimeSpan.TicksPerMillisecond / 1000; + const long MicrosecondsPerMillisecond = 1_000; + const long MicrosecondsPerSecond = 1_000_000; + const long MicrosecondsPerMinute = 60_000_000; + const long MicrosecondsPerHour = 3_600_000_000; + const long MicrosecondsPerDay = 86_400_000_000; + static TimeSpan TicksFromMicroseconds(decimal totalMicroseconds) + { + var ticks = totalMicroseconds * MicrosecondsToTicks; + if (ticks < long.MinValue || ticks > long.MaxValue) + { + throw new ArgumentOutOfRangeException(null, "TimeSpan overflowed because the duration is too long."); + } + return new((long)ticks); + } extension(TimeSpan) { /// /// Initializes a new instance of the structure to a specified number of days, hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromDays(int days, int hours = 0, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)days * TimeSpan.TicksPerDay + - (long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)days * MicrosecondsPerDay + + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromHours(int hours, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromMinutes(int minutes, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of seconds, milliseconds, and microseconds. /// public static TimeSpan FromSeconds(long seconds, long milliseconds = 0, long microseconds = 0) => - new(seconds * TimeSpan.TicksPerSecond + - milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of milliseconds and microseconds. /// public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0) => - new(milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of microseconds. /// public static TimeSpan FromMicroseconds(long microseconds) => - new(microseconds * MicrosecondsToTicks); + TicksFromMicroseconds(microseconds); } } diff --git a/src/Split/netstandard2.1/XDocumentPolyfill.cs b/src/Split/netstandard2.1/XDocumentPolyfill.cs index 1611acf19..876842223 100644 --- a/src/Split/netstandard2.1/XDocumentPolyfill.cs +++ b/src/Split/netstandard2.1/XDocumentPolyfill.cs @@ -25,12 +25,10 @@ static partial class XDocumentPolyfill /// /// Asynchronously creates a new XDocument and initializes its underlying XML tree using the specified stream, optionally preserving white space. /// - public static async Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) + public static Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - using var reader = new StreamReader(stream); - var content = await reader.ReadToEndAsync(); - return XDocument.Parse(content, options); + return Task.FromResult(XDocument.Load(stream, options)); } /// /// Asynchronously creates a new XDocument and initializes its underlying XML tree using the specified text reader, optionally preserving white space. diff --git a/src/Split/netstandard2.1/XElementPolyfill.cs b/src/Split/netstandard2.1/XElementPolyfill.cs index 27208879a..9347d9cdd 100644 --- a/src/Split/netstandard2.1/XElementPolyfill.cs +++ b/src/Split/netstandard2.1/XElementPolyfill.cs @@ -25,12 +25,10 @@ static partial class XElementPolyfill /// /// Asynchronously creates a new XElement and initializes its underlying XML tree using the specified stream, optionally preserving white space. /// - public static async Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) + public static Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - using var reader = new StreamReader(stream); - var content = await reader.ReadToEndAsync(); - return XElement.Parse(content, options); + return Task.FromResult(XElement.Load(stream, options)); } /// /// Asynchronously creates a new XElement and initializes its underlying XML tree using the specified text reader, optionally preserving white space. diff --git a/src/Split/netstandard2.1/ZipFilePolyfill.cs b/src/Split/netstandard2.1/ZipFilePolyfill.cs index 6cb361e6f..d908c5633 100644 --- a/src/Split/netstandard2.1/ZipFilePolyfill.cs +++ b/src/Split/netstandard2.1/ZipFilePolyfill.cs @@ -53,11 +53,12 @@ static void ExtractToDirectoryPolyfill( string destinationDirectory, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + return; } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, null); } static void ExtractToDirectoryPolyfill( string sourceArchiveFile, @@ -65,11 +66,43 @@ static void ExtractToDirectoryPolyfill( Encoding encoding, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); + return; + } + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, encoding); + } + static void ExtractToDirectoryWithOverwrite( + string sourceArchiveFile, + string destinationDirectory, + Encoding? encoding) + { + var destinationRoot = Path.GetFullPath(destinationDirectory); + Directory.CreateDirectory(destinationRoot); + var normalizedRoot = destinationRoot; + if (normalizedRoot[normalizedRoot.Length - 1] != Path.DirectorySeparatorChar) + { + normalizedRoot += Path.DirectorySeparatorChar; + } + using var archive = encoding == null + ? ZipFile.OpenRead(sourceArchiveFile) + : ZipFile.Open(sourceArchiveFile, ZipArchiveMode.Read, encoding); + foreach (var entry in archive.Entries) + { + var destinationPath = Path.GetFullPath(Path.Combine(destinationRoot, entry.FullName)); + if (!destinationPath.StartsWith(normalizedRoot, StringComparison.Ordinal)) + { + throw new IOException("Extracting Zip entry would have resulted in a file outside the specified destination directory."); + } + if (entry.Name.Length == 0) + { + Directory.CreateDirectory(destinationPath); + continue; + } + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + entry.ExtractToFile(destinationPath, overwrite: true); } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); } /// /// Asynchronously opens a ZipArchive on the specified archiveFileName in the specified ZipArchiveMode mode. diff --git a/src/Split/uap10.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs b/src/Split/uap10.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs index 32b0afded..8bad1d83d 100644 --- a/src/Split/uap10.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs +++ b/src/Split/uap10.0/ArgumentExceptions/ArgumentOutOfRangeExceptionPolyfill.cs @@ -15,7 +15,7 @@ public static void ThrowIfZero(T value, [CallerArgumentExpression(nameof(valu { if (value.Equals(default)) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } /// Throws an if is zero. @@ -23,12 +23,12 @@ public static void ThrowIfZero(nint value, [CallerArgumentExpression(nameof(valu { if (value == 0) { - ThrowZero(paramName); + ThrowZero(value, paramName); } } [DoesNotReturn] - static void ThrowZero(string? paramName) => - throw new ArgumentOutOfRangeException(paramName, "Value must not be zero."); + static void ThrowZero(T value, string? paramName) => + throw new ArgumentOutOfRangeException(paramName, value, $"{paramName} ('{value}') must be a non-zero value."); public static void ThrowIfNegative(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : struct, IComparable { @@ -112,7 +112,7 @@ public static void ThrowIfGreaterThanOrEqual(nint value, nint other, [CallerArgu { if (value >= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be less than {other}."); } } public static void ThrowIfLessThan(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) @@ -144,7 +144,7 @@ public static void ThrowIfLessThanOrEqual(nint value, nint other, [CallerArgumen { if (value <= other) { - throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than or equal to {other}."); + throw new ArgumentOutOfRangeException(paramName, value, $"Value must be greater than {other}."); } } } diff --git a/src/Split/uap10.0/ConvertPolyfill.cs b/src/Split/uap10.0/ConvertPolyfill.cs index 412674710..1f45391f9 100644 --- a/src/Split/uap10.0/ConvertPolyfill.cs +++ b/src/Split/uap10.0/ConvertPolyfill.cs @@ -134,19 +134,19 @@ public static OperationStatus FromHexString(ReadOnlySpan source, Span= destination.Length) { charsConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal(source[i]); + var lo = GetHexVal(source[i + 1]); + if (hi < 0 || lo < 0) { - charsConsumed = i; + charsConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; @@ -193,19 +193,19 @@ public static OperationStatus FromHexString(ReadOnlySpan utf8Source, Span< var j = 0; while (i + 1 < utf8Source.Length) { - var hi = GetHexVal((char)utf8Source[i]); - var lo = GetHexVal((char)utf8Source[i + 1]); - if (hi < 0 || lo < 0) + if (j >= destination.Length) { bytesConsumed = i; bytesWritten = j; - return OperationStatus.InvalidData; + return OperationStatus.DestinationTooSmall; } - if (j >= destination.Length) + var hi = GetHexVal((char)utf8Source[i]); + var lo = GetHexVal((char)utf8Source[i + 1]); + if (hi < 0 || lo < 0) { - bytesConsumed = i; + bytesConsumed = lo < 0 ? i + 1 : i; bytesWritten = j; - return OperationStatus.DestinationTooSmall; + return OperationStatus.InvalidData; } destination[j++] = (byte)((hi << 4) | lo); i += 2; diff --git a/src/Split/uap10.0/DateTimeOffsetPolyfill.cs b/src/Split/uap10.0/DateTimeOffsetPolyfill.cs index 6837790e5..012b944dc 100644 --- a/src/Split/uap10.0/DateTimeOffsetPolyfill.cs +++ b/src/Split/uap10.0/DateTimeOffsetPolyfill.cs @@ -17,7 +17,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTimeOffset(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Offset); diff --git a/src/Split/uap10.0/DateTimePolyfill.cs b/src/Split/uap10.0/DateTimePolyfill.cs index 4641f3e59..430683e14 100644 --- a/src/Split/uap10.0/DateTimePolyfill.cs +++ b/src/Split/uap10.0/DateTimePolyfill.cs @@ -16,7 +16,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microsecond => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new DateTime(target.Year, target.Month, target.Day, target.Hour, target.Minute, 0, target.Kind); diff --git a/src/Split/uap10.0/EqualityComparerPolyfill.cs b/src/Split/uap10.0/EqualityComparerPolyfill.cs index e45f2a30e..55cb3ad44 100644 --- a/src/Split/uap10.0/EqualityComparerPolyfill.cs +++ b/src/Split/uap10.0/EqualityComparerPolyfill.cs @@ -19,8 +19,14 @@ public override int GetHashCode(T obj) => /// public static EqualityComparer Create( Func equals, - Func? getHashCode = null) => - new DelegateEqualityComparer(equals, getHashCode); + Func? getHashCode = null) + { + if (equals is null) + { + throw new ArgumentNullException(nameof(equals)); + } + return new DelegateEqualityComparer(equals, getHashCode); + } } class KeySelectorEqualityComparer(Func keySelector, IEqualityComparer? keyComparer) : EqualityComparer @@ -41,7 +47,13 @@ public override int GetHashCode(T obj) /// public static EqualityComparer Create( Func keySelector, - IEqualityComparer? keyComparer = null) => - new KeySelectorEqualityComparer(keySelector, keyComparer); + IEqualityComparer? keyComparer = null) + { + if (keySelector is null) + { + throw new ArgumentNullException(nameof(keySelector)); + } + return new KeySelectorEqualityComparer(keySelector, keyComparer); + } } } diff --git a/src/Split/uap10.0/FileUnixModePolyfill.cs b/src/Split/uap10.0/FileUnixModePolyfill.cs index 2440f7800..701ef07cb 100644 --- a/src/Split/uap10.0/FileUnixModePolyfill.cs +++ b/src/Split/uap10.0/FileUnixModePolyfill.cs @@ -47,8 +47,8 @@ public static UnixFileMode GetUnixFileMode(string path) var mode = output[0] switch { '1' => UnixFileMode.StickyBit, - '2' => UnixFileMode.SetUser, - '4' => UnixFileMode.SetGroup, + '2' => UnixFileMode.SetGroup, + '4' => UnixFileMode.SetUser, _ => UnixFileMode.None }; mode = output[1] switch @@ -158,13 +158,13 @@ public static void SetUnixFileMode(string path, UnixFileMode mode) octal[3] += 1; break; case "setuser": - octal[0] += 2; + octal[0] += 4; break; case "stickybit": octal[0] += 1; break; case "setgroup": - octal[0] += 4; + octal[0] += 2; break; default: throw new("Invalid notation detected"); diff --git a/src/Split/uap10.0/Numbers/DoublePolyfill.cs b/src/Split/uap10.0/Numbers/DoublePolyfill.cs index ba5333f69..2e3e40efd 100644 --- a/src/Split/uap10.0/Numbers/DoublePolyfill.cs +++ b/src/Split/uap10.0/Numbers/DoublePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out double result) => - double.TryParse(s, NumberStyles.Float, provider, out result); + double.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,7 +28,7 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its double-precision floating-point number equivalent.. /// public static bool TryParse(ReadOnlySpan utf8Text, out double result) => - double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + double.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Converts the span representation of a number in a specified style and culture-specific format to its double-precision floating-point number equivalent. A return value indicates whether the conversion succeeded or failed. /// @@ -43,7 +43,7 @@ public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatPro /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out double result) => - double.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + double.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/uap10.0/Numbers/SinglePolyfill.cs b/src/Split/uap10.0/Numbers/SinglePolyfill.cs index 69af78095..2f6e02a82 100644 --- a/src/Split/uap10.0/Numbers/SinglePolyfill.cs +++ b/src/Split/uap10.0/Numbers/SinglePolyfill.cs @@ -12,13 +12,13 @@ static partial class Polyfill /// Tries to parse a string into a value. /// public static bool TryParse(string? s, IFormatProvider? provider, out float result) => - float.TryParse(s, NumberStyles.Float, provider, out result); + float.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #if FeatureMemory /// /// Tries to parse a span of UTF-8 characters into a value. /// public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, provider, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); /// /// Tries to parse a span of UTF-8 characters into a value. /// @@ -28,7 +28,7 @@ public static bool TryParse(ReadOnlySpan utf8Text, NumberStyles style, IFo /// Tries to convert a UTF-8 character span containing the string representation of a number to its single-precision floating-point number equivalent. /// public static bool TryParse(ReadOnlySpan utf8Text, out float result) => - float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float, null, out result); + float.TryParse(Encoding.UTF8.GetString(utf8Text), NumberStyles.Float | NumberStyles.AllowThousands,null, out result); /// /// Converts the span representation of a number in a specified style and culture-specific format to its single-precision floating-point number equivalent. A return value indicates whether the conversion succeeded or failed. /// @@ -43,7 +43,7 @@ public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatPro /// Tries to parse a span of characters into a value. /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out float result) => - float.TryParse(s.ToString(), NumberStyles.Float, provider, out result); + float.TryParse(s.ToString(), NumberStyles.Float | NumberStyles.AllowThousands,provider, out result); #endif } } diff --git a/src/Split/uap10.0/Polyfill.cs b/src/Split/uap10.0/Polyfill.cs index 16e916262..b7b21af17 100644 --- a/src/Split/uap10.0/Polyfill.cs +++ b/src/Split/uap10.0/Polyfill.cs @@ -17,5 +17,5 @@ namespace Polyfills; #endif static partial class Polyfill { - const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond * 1000; + const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; } diff --git a/src/Split/uap10.0/Polyfill_ArraySegment.cs b/src/Split/uap10.0/Polyfill_ArraySegment.cs index 96e83c18d..05ea1f659 100644 --- a/src/Split/uap10.0/Polyfill_ArraySegment.cs +++ b/src/Split/uap10.0/Polyfill_ArraySegment.cs @@ -12,15 +12,19 @@ static partial class Polyfill /// public static void CopyTo(this ArraySegment target, ArraySegment destination) { - if (target.Count > destination.Count) + if (target.Array == null) { - throw new ArgumentException("DestinationTooShort", nameof(destination)); + throw new InvalidOperationException("InvalidOperation_NullArray"); } - if (target.Array == null) + if (destination.Array == null) { throw new InvalidOperationException("InvalidOperation_NullArray"); } - Array.Copy(target.Array!, target.Offset, destination.Array, destination.Offset, target.Count); + if (target.Count > destination.Count) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } + Array.Copy(target.Array, target.Offset, destination.Array, destination.Offset, target.Count); } /// /// Copies the contents of this instance into the specified destination array of the same type T. diff --git a/src/Split/uap10.0/Polyfill_ConcurrentDictionary.cs b/src/Split/uap10.0/Polyfill_ConcurrentDictionary.cs index 8805de34b..a3547dd29 100644 --- a/src/Split/uap10.0/Polyfill_ConcurrentDictionary.cs +++ b/src/Split/uap10.0/Polyfill_ConcurrentDictionary.cs @@ -12,6 +12,10 @@ static partial class Polyfill public static TValue GetOrAdd(this ConcurrentDictionary target, TKey key, Func valueFactory, TArg factoryArgument) where TKey : notnull { + if (valueFactory == null) + { + throw new ArgumentNullException(nameof(valueFactory)); + } while (true) { TValue value; diff --git a/src/Split/uap10.0/Polyfill_Encoding_GetChars.cs b/src/Split/uap10.0/Polyfill_Encoding_GetChars.cs index f5a7de80b..4b956e845 100644 --- a/src/Split/uap10.0/Polyfill_Encoding_GetChars.cs +++ b/src/Split/uap10.0/Polyfill_Encoding_GetChars.cs @@ -25,7 +25,7 @@ public static int GetChars(this Encoding target, ReadOnlySpan bytes, Span< var charArray = new char[bytes.Length]; var array = bytes.ToArray(); var count = target.GetChars(array, 0, bytes.Length, charArray, 0); - new ReadOnlySpan(charArray).CopyTo(chars); + new ReadOnlySpan(charArray, 0, count).CopyTo(chars); return count; } #endif diff --git a/src/Split/uap10.0/Polyfill_List.cs b/src/Split/uap10.0/Polyfill_List.cs index c2e76ed77..7a9348d0c 100644 --- a/src/Split/uap10.0/Polyfill_List.cs +++ b/src/Split/uap10.0/Polyfill_List.cs @@ -21,6 +21,10 @@ public static void AddRange(this List target, ReadOnlySpan source) /// Inserts the elements of a span into the at the specified index. public static void InsertRange(this List target, int index, ReadOnlySpan source) { + if ((uint)index > (uint)target.Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } for (var i = 0; i < source.Length; i++) { var item = source[i]; @@ -30,6 +34,10 @@ public static void InsertRange(this List target, int index, ReadOnlySpanCopies the entire to a span. public static void CopyTo(this List target, Span destination) { + if (target.Count > destination.Length) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } for (var index = 0; index < target.Count; index++) { destination[index] = target[index]; diff --git a/src/Split/uap10.0/Polyfill_Memory_SpanSort.cs b/src/Split/uap10.0/Polyfill_Memory_SpanSort.cs index acc5c8666..b30957972 100644 --- a/src/Split/uap10.0/Polyfill_Memory_SpanSort.cs +++ b/src/Split/uap10.0/Polyfill_Memory_SpanSort.cs @@ -77,12 +77,8 @@ internal ComparerWrapper(Comparison comparison) throw new ArgumentNullException(nameof(comparison)); this.comparison = comparison; } - public int Compare(T? x, T? y) - { - if (x is null || y is null) - return 0; - return comparison((T)x, (T)y); - } + public int Compare(T? x, T? y) => + comparison(x!, y!); } } #endif diff --git a/src/Split/uap10.0/Polyfill_MicroNanosecondAdd.cs b/src/Split/uap10.0/Polyfill_MicroNanosecondAdd.cs index 17d2c8d00..e099e1260 100644 --- a/src/Split/uap10.0/Polyfill_MicroNanosecondAdd.cs +++ b/src/Split/uap10.0/Polyfill_MicroNanosecondAdd.cs @@ -8,10 +8,10 @@ static partial class Polyfill /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTime AddMicroseconds(this DateTime target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); /// /// Returns a new object that adds a specified number of microseconds to the value of this instance.. /// public static DateTimeOffset AddMicroseconds(this DateTimeOffset target, double microseconds) => - target.AddMilliseconds(microseconds / 1000); + target.AddTicks((long)(microseconds * 10)); } diff --git a/src/Split/uap10.0/Polyfill_String.cs b/src/Split/uap10.0/Polyfill_String.cs index bab55956e..254f4be0a 100644 --- a/src/Split/uap10.0/Polyfill_String.cs +++ b/src/Split/uap10.0/Polyfill_String.cs @@ -3,8 +3,6 @@ namespace Polyfills; using System; using System.Text; -using System.IO; -using System.Runtime.CompilerServices; static partial class Polyfill { #if FeatureMemory @@ -147,22 +145,49 @@ public static int LastIndexOf(this string target, char value, int startIndex, in /// /// Replaces all newline sequences in the current string with . /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ReplaceLineEndings(this string target, string replacementText) { + var index = IndexOfNewlineChar(target, 0, out var stride); + if (index < 0) + { + return target; + } var builder = new StringBuilder(target.Length); - using var reader = new StringReader(target); + var position = 0; while (true) { - var line = reader.ReadLine(); - if (line == null) + builder.Append(target, position, index - position); + builder.Append(replacementText); + position = index + stride; + index = IndexOfNewlineChar(target, position, out stride); + if (index < 0) { break; } - builder.Append(line); - builder.Append(replacementText); } - return builder.ToString(0, builder.Length - replacementText.Length); + builder.Append(target, position, target.Length - position); + return builder.ToString(); + } + static int IndexOfNewlineChar(string text, int startIndex, out int stride) + { + stride = 0; + for (var index = startIndex; index < text.Length; index++) + { + switch (text[index]) + { + case '\r': + stride = index + 1 < text.Length && text[index + 1] == '\n' ? 2 : 1; + return index; + case '\n': + case '\f': + case '\u0085': + case '\u2028': + case '\u2029': + stride = 1; + return index; + } + } + return -1; } /// /// Replaces all newline sequences in the current string with . diff --git a/src/Split/uap10.0/Polyfill_StringBuilder_Append.cs b/src/Split/uap10.0/Polyfill_StringBuilder_Append.cs index 0b60749a9..d3970ce53 100644 --- a/src/Split/uap10.0/Polyfill_StringBuilder_Append.cs +++ b/src/Split/uap10.0/Polyfill_StringBuilder_Append.cs @@ -10,6 +10,14 @@ static partial class Polyfill /// public static StringBuilder Append(this StringBuilder target, StringBuilder? value, int startIndex, int count) { + if (startIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(startIndex)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } if (value == null) { if (startIndex == 0 && count == 0) diff --git a/src/Split/uap10.0/Polyfill_StringBuilder_CopyTo.cs b/src/Split/uap10.0/Polyfill_StringBuilder_CopyTo.cs index dc625a7bc..ba99e0a1f 100644 --- a/src/Split/uap10.0/Polyfill_StringBuilder_CopyTo.cs +++ b/src/Split/uap10.0/Polyfill_StringBuilder_CopyTo.cs @@ -15,20 +15,25 @@ public static void CopyTo( Span destination, int count) { - var destinationIndex = 0; - while (true) + if (count < 0) { - if (sourceIndex == target.Length) - { - break; - } - if (destinationIndex == count) - { - break; - } - destination[destinationIndex] = target[sourceIndex]; - destinationIndex++; - sourceIndex++; + throw new ArgumentOutOfRangeException(nameof(count)); + } + if ((uint)sourceIndex > (uint)target.Length) + { + throw new ArgumentOutOfRangeException(nameof(sourceIndex)); + } + if (sourceIndex > target.Length - count) + { + throw new ArgumentException("The source StringBuilder does not contain enough characters from sourceIndex onward to satisfy count."); + } + if (count > destination.Length) + { + throw new ArgumentException("The destination span is too short to hold the requested characters.", nameof(destination)); + } + for (var index = 0; index < count; index++) + { + destination[index] = target[sourceIndex + index]; } } } diff --git a/src/Split/uap10.0/Polyfill_Type.cs b/src/Split/uap10.0/Polyfill_Type.cs index 7890e2168..b99bdfab5 100644 --- a/src/Split/uap10.0/Polyfill_Type.cs +++ b/src/Split/uap10.0/Polyfill_Type.cs @@ -7,9 +7,15 @@ namespace Polyfills; using System.Linq; static partial class Polyfill { - public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInfo other) => - target.MetadataToken == other.MetadataToken && - target.Module.Equals(other.Module); + public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInfo other) + { + if (other is null) + { + throw new ArgumentNullException(nameof(other)); + } + return target.MetadataToken == other.MetadataToken && + target.Module.Equals(other.Module); + } /// /// Searches for the specified method whose parameters match the specified generic parameter count, argument types and modifiers, using the specified binding constraints. /// @@ -22,6 +28,25 @@ public static bool HasSameMetadataDefinitionAs(this MemberInfo target, MemberInf Type[] types, ParameterModifier[]? modifiers) { + if (name is null) + { + throw new ArgumentNullException(nameof(name)); + } + if (genericParameterCount < 0) + { + throw new ArgumentOutOfRangeException(nameof(genericParameterCount)); + } + if (types is null) + { + throw new ArgumentNullException(nameof(types)); + } + foreach (var type in types) + { + if (type is null) + { + throw new ArgumentNullException(nameof(types)); + } + } var methods = target.GetMethods(bindingAttr); if (genericParameterCount == 0) { diff --git a/src/Split/uap10.0/RandomNumberGeneratorPolyfill.cs b/src/Split/uap10.0/RandomNumberGeneratorPolyfill.cs index 08384a7ea..539aa7aba 100644 --- a/src/Split/uap10.0/RandomNumberGeneratorPolyfill.cs +++ b/src/Split/uap10.0/RandomNumberGeneratorPolyfill.cs @@ -34,7 +34,7 @@ public static int GetInt32(int fromInclusive, int toExclusive) { generator.GetBytes(bytes); value = BitConverter.ToUInt32(bytes, 0) & mask; - } while (value >= range); + } while (value > range); return (int) (fromInclusive + value); } /// diff --git a/src/Split/uap10.0/StringPolyfill.cs b/src/Split/uap10.0/StringPolyfill.cs index 6ae5df100..6412adfc4 100644 --- a/src/Split/uap10.0/StringPolyfill.cs +++ b/src/Split/uap10.0/StringPolyfill.cs @@ -50,6 +50,10 @@ public static string Join(char separator, scoped ReadOnlySpan values) span = span.Slice(1); } var value = values[index]; + if (value is null) + { + continue; + } value.CopyTo(span); span = span.Slice(value.Length); } diff --git a/src/Split/uap10.0/TaskCompletionSource.cs b/src/Split/uap10.0/TaskCompletionSource.cs index 04b507f76..67a35f938 100644 --- a/src/Split/uap10.0/TaskCompletionSource.cs +++ b/src/Split/uap10.0/TaskCompletionSource.cs @@ -77,5 +77,5 @@ public TaskCompletionSource(object? state, TaskCreationOptions creationOptions) /// /// Attempts to transition the underlying into the state. /// - public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(default); + public bool TrySetCanceled(CancellationToken cancellationToken) => inner.TrySetCanceled(cancellationToken); } diff --git a/src/Split/uap10.0/TimeSpanPolyfill.cs b/src/Split/uap10.0/TimeSpanPolyfill.cs index 94d13cbbb..59d7c7974 100644 --- a/src/Split/uap10.0/TimeSpanPolyfill.cs +++ b/src/Split/uap10.0/TimeSpanPolyfill.cs @@ -15,7 +15,7 @@ static partial class Polyfill /// Gets the microsecond component of the time represented by the current object. /// public int Microseconds => - (int) (target.TicksComponent() % TicksPerMicrosecond) * 1000; + (int) (target.TicksComponent() / TicksPerMicrosecond % 1000); long TicksComponent() { var noSeconds = new TimeSpan(target.Days, target.Hours, target.Minutes, 0); @@ -24,52 +24,71 @@ long TicksComponent() } } const long MicrosecondsToTicks = TimeSpan.TicksPerMillisecond / 1000; + const long MicrosecondsPerMillisecond = 1_000; + const long MicrosecondsPerSecond = 1_000_000; + const long MicrosecondsPerMinute = 60_000_000; + const long MicrosecondsPerHour = 3_600_000_000; + const long MicrosecondsPerDay = 86_400_000_000; + static TimeSpan TicksFromMicroseconds(decimal totalMicroseconds) + { + var ticks = totalMicroseconds * MicrosecondsToTicks; + if (ticks < long.MinValue || ticks > long.MaxValue) + { + throw new ArgumentOutOfRangeException(null, "TimeSpan overflowed because the duration is too long."); + } + return new((long)ticks); + } extension(TimeSpan) { /// /// Initializes a new instance of the structure to a specified number of days, hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromDays(int days, int hours = 0, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)days * TimeSpan.TicksPerDay + - (long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)days * MicrosecondsPerDay + + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of hours, minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromHours(int hours, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)hours * TimeSpan.TicksPerHour + - (long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)hours * MicrosecondsPerHour + + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of minutes, seconds, milliseconds, and microseconds. /// public static TimeSpan FromMinutes(int minutes, int seconds = 0, int milliseconds = 0, int microseconds = 0) => - new((long)minutes * TimeSpan.TicksPerMinute + - (long)seconds * TimeSpan.TicksPerSecond + - (long)milliseconds * TimeSpan.TicksPerMillisecond + - (long)microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)minutes * MicrosecondsPerMinute + + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of seconds, milliseconds, and microseconds. /// public static TimeSpan FromSeconds(long seconds, long milliseconds = 0, long microseconds = 0) => - new(seconds * TimeSpan.TicksPerSecond + - milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)seconds * MicrosecondsPerSecond + + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of milliseconds and microseconds. /// public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0) => - new(milliseconds * TimeSpan.TicksPerMillisecond + - microseconds * MicrosecondsToTicks); + TicksFromMicroseconds( + (decimal)milliseconds * MicrosecondsPerMillisecond + + microseconds); /// /// Initializes a new instance of the structure to a specified number of microseconds. /// public static TimeSpan FromMicroseconds(long microseconds) => - new(microseconds * MicrosecondsToTicks); + TicksFromMicroseconds(microseconds); } } diff --git a/src/Split/uap10.0/XDocumentPolyfill.cs b/src/Split/uap10.0/XDocumentPolyfill.cs index 1611acf19..876842223 100644 --- a/src/Split/uap10.0/XDocumentPolyfill.cs +++ b/src/Split/uap10.0/XDocumentPolyfill.cs @@ -25,12 +25,10 @@ static partial class XDocumentPolyfill /// /// Asynchronously creates a new XDocument and initializes its underlying XML tree using the specified stream, optionally preserving white space. /// - public static async Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) + public static Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - using var reader = new StreamReader(stream); - var content = await reader.ReadToEndAsync(); - return XDocument.Parse(content, options); + return Task.FromResult(XDocument.Load(stream, options)); } /// /// Asynchronously creates a new XDocument and initializes its underlying XML tree using the specified text reader, optionally preserving white space. diff --git a/src/Split/uap10.0/XElementPolyfill.cs b/src/Split/uap10.0/XElementPolyfill.cs index 27208879a..9347d9cdd 100644 --- a/src/Split/uap10.0/XElementPolyfill.cs +++ b/src/Split/uap10.0/XElementPolyfill.cs @@ -25,12 +25,10 @@ static partial class XElementPolyfill /// /// Asynchronously creates a new XElement and initializes its underlying XML tree using the specified stream, optionally preserving white space. /// - public static async Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) + public static Task LoadAsync(Stream stream, LoadOptions options, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - using var reader = new StreamReader(stream); - var content = await reader.ReadToEndAsync(); - return XElement.Parse(content, options); + return Task.FromResult(XElement.Load(stream, options)); } /// /// Asynchronously creates a new XElement and initializes its underlying XML tree using the specified text reader, optionally preserving white space. diff --git a/src/Split/uap10.0/ZipFilePolyfill.cs b/src/Split/uap10.0/ZipFilePolyfill.cs index 6cb361e6f..d908c5633 100644 --- a/src/Split/uap10.0/ZipFilePolyfill.cs +++ b/src/Split/uap10.0/ZipFilePolyfill.cs @@ -53,11 +53,12 @@ static void ExtractToDirectoryPolyfill( string destinationDirectory, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + return; } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory); + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, null); } static void ExtractToDirectoryPolyfill( string sourceArchiveFile, @@ -65,11 +66,43 @@ static void ExtractToDirectoryPolyfill( Encoding encoding, bool overwrite) { - if (overwrite && Directory.Exists(destinationDirectory)) + if (!overwrite) { - Directory.Delete(destinationDirectory, true); + ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); + return; + } + ExtractToDirectoryWithOverwrite(sourceArchiveFile, destinationDirectory, encoding); + } + static void ExtractToDirectoryWithOverwrite( + string sourceArchiveFile, + string destinationDirectory, + Encoding? encoding) + { + var destinationRoot = Path.GetFullPath(destinationDirectory); + Directory.CreateDirectory(destinationRoot); + var normalizedRoot = destinationRoot; + if (normalizedRoot[normalizedRoot.Length - 1] != Path.DirectorySeparatorChar) + { + normalizedRoot += Path.DirectorySeparatorChar; + } + using var archive = encoding == null + ? ZipFile.OpenRead(sourceArchiveFile) + : ZipFile.Open(sourceArchiveFile, ZipArchiveMode.Read, encoding); + foreach (var entry in archive.Entries) + { + var destinationPath = Path.GetFullPath(Path.Combine(destinationRoot, entry.FullName)); + if (!destinationPath.StartsWith(normalizedRoot, StringComparison.Ordinal)) + { + throw new IOException("Extracting Zip entry would have resulted in a file outside the specified destination directory."); + } + if (entry.Name.Length == 0) + { + Directory.CreateDirectory(destinationPath); + continue; + } + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + entry.ExtractToFile(destinationPath, overwrite: true); } - ZipFile.ExtractToDirectory(sourceArchiveFile, destinationDirectory, encoding); } /// /// Asynchronously opens a ZipArchive on the specified archiveFileName in the specified ZipArchiveMode mode. diff --git a/src/Tests/PolyfillTests_Encoding.cs b/src/Tests/PolyfillTests_Encoding.cs index ad325003c..c767a3467 100644 --- a/src/Tests/PolyfillTests_Encoding.cs +++ b/src/Tests/PolyfillTests_Encoding.cs @@ -44,6 +44,23 @@ public async Task Encoding_GetChars() await Assert.That(result).IsEqualTo("Hello, World!"); } + [Test] + public async Task Encoding_GetChars_MultiByte() + { + var encoding = Encoding.UTF8; + var text = "héllo wörld"; + var utf8Bytes = encoding.GetBytes(text); + // Decoded char count is smaller than the byte count for multi-byte input, + // so a destination sized to the char count must not be over-written. + var charCount = encoding.GetCharCount(utf8Bytes); + var charArray = new char[charCount]; + var written = encoding.GetChars(new ReadOnlySpan(utf8Bytes), new Span(charArray)); + + await Assert.That(charCount).IsLessThan(utf8Bytes.Length); + await Assert.That(written).IsEqualTo(charCount); + await Assert.That(new string(charArray)).IsEqualTo(text); + } + [Test] public async Task Encoding_GetString() { diff --git a/src/Tests/PolyfillTests_Memory_Sort.cs b/src/Tests/PolyfillTests_Memory_Sort.cs index 1c45f839e..5e3af69b1 100644 --- a/src/Tests/PolyfillTests_Memory_Sort.cs +++ b/src/Tests/PolyfillTests_Memory_Sort.cs @@ -87,6 +87,22 @@ public async Task SpanSort_KeysAndValues_WithComparison() await Assert.That(expectedValues.SequenceEqual(values)).IsTrue(); } + [Test] + public async Task SpanSort_KeysAndValues_WithComparison_NullKeys() + { + // null keys must be ordered by the supplied comparison, not treated as equal-to-everything + string?[] keys = ["b", null, "a"]; + var values = new[] {0, 1, 2}; + string?[] expectedKeys = [null, "a", "b"]; + var expectedValues = new[] {1, 2, 0}; + + keys.AsSpan().Sort(values.AsSpan(), (x, y) => string.CompareOrdinal(x, y)); + + // Compare as joined strings to avoid the span SequenceEqual overload, which NREs on null elements on net48. + await Assert.That(string.Join(",", keys.Select(_ => _ ?? "null"))).IsEqualTo(string.Join(",", expectedKeys.Select(_ => _ ?? "null"))); + await Assert.That(string.Join(",", values)).IsEqualTo(string.Join(",", expectedValues)); + } + #if !NET5_0_OR_GREATER [Test] public async Task SpanSort_NullComparison_ThrowsArgumentNullException() diff --git a/src/Tests/PolyfillTests_MicroNanosecond.cs b/src/Tests/PolyfillTests_MicroNanosecond.cs index 7f3ba28c9..e81a6c36d 100644 --- a/src/Tests/PolyfillTests_MicroNanosecond.cs +++ b/src/Tests/PolyfillTests_MicroNanosecond.cs @@ -15,23 +15,34 @@ public async Task AddMicroseconds() await Assert.That(fromMicrosecondsDateTime).IsEqualTo(fromTicksDateTime); } -#if NET7_0_OR_GREATER - static TimeSpan timeSpan = DateTime.Now.TimeOfDay; [Test] - public async Task Nanoseconds() + public async Task AddMicroseconds_SubMillisecond() { - await Assert.That(dateTimeOffset.Nanosecond).IsEqualTo(dateTimeOffset.Nanosecond); - await Assert.That(timeSpan.Nanoseconds).IsEqualTo(timeSpan.Nanoseconds); - await Assert.That(dateTime.Nanosecond).IsEqualTo(dateTime.Nanosecond); + // 1 microsecond == 10 ticks; sub-millisecond precision must be preserved + // (AddMilliseconds rounds to whole milliseconds on .NET Framework). + var baseDateTime = new DateTime(2020, 1, 1); + await Assert.That((baseDateTime.AddMicroseconds(500) - baseDateTime).Ticks).IsEqualTo(5000L); + await Assert.That((baseDateTime.AddMicroseconds(1) - baseDateTime).Ticks).IsEqualTo(10L); + + var baseOffset = new DateTimeOffset(2020, 1, 1, 0, 0, 0, TimeSpan.Zero); + await Assert.That((baseOffset.AddMicroseconds(500) - baseOffset).Ticks).IsEqualTo(5000L); + await Assert.That((baseOffset.AddMicroseconds(1) - baseOffset).Ticks).IsEqualTo(10L); } [Test] - public async Task Microsecond() + public async Task Microsecond_and_Nanosecond() { - await Assert.That(dateTimeOffset.Microsecond).IsEqualTo(dateTimeOffset.Microsecond); - await Assert.That(timeSpan.Microseconds).IsEqualTo(timeSpan.Microseconds); - await Assert.That(dateTime.Microsecond).IsEqualTo(dateTime.Microsecond); - } -#endif + // 1234567 sub-second ticks decompose to 123 ms, 456 µs, 700 ns + var dateTimeValue = new DateTime(2020, 1, 1, 12, 30, 45).AddTicks(1234567); + await Assert.That(dateTimeValue.Microsecond).IsEqualTo(456); + await Assert.That(dateTimeValue.Nanosecond).IsEqualTo(700); + var dateTimeOffsetValue = new DateTimeOffset(dateTimeValue, TimeSpan.Zero); + await Assert.That(dateTimeOffsetValue.Microsecond).IsEqualTo(456); + await Assert.That(dateTimeOffsetValue.Nanosecond).IsEqualTo(700); + + var timeSpanValue = new TimeSpan(1234567); + await Assert.That(timeSpanValue.Microseconds).IsEqualTo(456); + await Assert.That(timeSpanValue.Nanoseconds).IsEqualTo(700); + } } \ No newline at end of file diff --git a/src/Tests/PolyfillTests_Pass2.cs b/src/Tests/PolyfillTests_Pass2.cs new file mode 100644 index 000000000..d90f96705 --- /dev/null +++ b/src/Tests/PolyfillTests_Pass2.cs @@ -0,0 +1,299 @@ +using System.Buffers; +using System.IO.Compression; +using System.Security.Cryptography; + +// Regression tests for the second-pass bug fixes. Each runs on every target framework, +// exercising the polyfill on older TFMs and the BCL on newer ones. +partial class PolyfillTests +{ + [Test] + public async Task ConcurrentDictionary_GetOrAdd_NullFactory_Throws() + { + var dictionary = new ConcurrentDictionary(); + dictionary.TryAdd("a", 1); + Func factory = null!; + + // Eager validation: throws for both present and absent keys. + await Assert.That(() => dictionary.GetOrAdd("a", factory, 5)).Throws(); + await Assert.That(() => dictionary.GetOrAdd("b", factory, 5)).Throws(); + } + + [Test] + public async Task List_CopyTo_TooShort_Throws_AndLeavesDestinationUntouched() + { + var list = new List {10, 20, 30, 40, 50}; + + var destination = new int[3]; + await Assert.That(() => list.CopyTo(new Span(destination))).Throws(); + await Assert.That(destination.All(_ => _ == 0)).IsTrue(); + + var ok = new int[5]; + list.CopyTo(new Span(ok)); + await Assert.That(string.Join(",", ok)).IsEqualTo("10,20,30,40,50"); + } + + [Test] + public async Task List_InsertRange_InvalidIndex_Throws() + { + var list = new List {1, 2, 3}; + await Assert.That(() => list.InsertRange(10, ReadOnlySpan.Empty)).Throws(); + } + + [Test] + public async Task EqualityComparer_Create_NullDelegates_Throw() + { + await Assert.That(() => EqualityComparer.Create(null!)).Throws(); + + // The keySelector overload's delegate signature differs in nullability between the + // polyfill (Func) and the .NET 11 BCL (Func); adapt accordingly. +#if NET11_0_OR_GREATER + Func keySelector = null!; +#else + Func keySelector = null!; +#endif + await Assert.That(() => EqualityComparer.Create(keySelector)).Throws(); + } + + [Test] + public async Task FloatingPoint_TryParse_AllowsThousands() + { + var invariant = CultureInfo.InvariantCulture; + + await Assert.That(double.TryParse("1,234.5", invariant, out var d)).IsTrue(); + await Assert.That(d).IsEqualTo(1234.5); + + await Assert.That(double.TryParse("1,234.5".AsSpan(), invariant, out var d2)).IsTrue(); + await Assert.That(d2).IsEqualTo(1234.5); + + await Assert.That(float.TryParse("1,234.5", invariant, out var f)).IsTrue(); + await Assert.That(f).IsEqualTo(1234.5f); + + await Assert.That(float.TryParse("1,234.5".AsSpan(), invariant, out var f2)).IsTrue(); + await Assert.That(f2).IsEqualTo(1234.5f); + } + + [Test] + public async Task ArraySegment_CopyTo_DefaultDestination_Throws() + { + var source = new ArraySegment(new[] {1, 2, 3}); + ArraySegment destination = default; + await Assert.That(() => source.CopyTo(destination)).Throws(); + } + + [Test] + public async Task TimeSpan_From_HappyPath_And_Overflow() + { + await Assert.That(TimeSpan.FromMicroseconds(1000000).Ticks).IsEqualTo(TimeSpan.TicksPerSecond); + await Assert.That(TimeSpan.FromDays(1).Ticks).IsEqualTo(TimeSpan.TicksPerDay); + await Assert.That(TimeSpan.FromSeconds(1, 0, 0).Ticks).IsEqualTo(TimeSpan.TicksPerSecond); + + // Components that cancel out must stay in range. Accumulation happens in a wide (decimal) + // space, so even when an individual component product overflows Int64, a cancelling + // component keeps the result in range (matching the BCL's 128-bit accumulation). + await Assert.That(TimeSpan.FromSeconds(1000000000000, -1000000000000000, 0).Ticks).IsEqualTo(0L); + await Assert.That(TimeSpan.FromSeconds(0, -9498038399810654, long.MaxValue).Ticks).IsEqualTo(-2746663629558781930L); + + // The multi-argument overloads reach the integer-based factory on every framework + // (the single-argument FromMicroseconds(double) BCL overload shadows the polyfill on net7/8). + await Assert.That(() => TimeSpan.FromDays(int.MaxValue, 0, 0, 0, 0, 0)).Throws(); + await Assert.That(() => TimeSpan.FromSeconds(long.MaxValue, 0, 0)).Throws(); + } + + [Test] + public async Task Type_GetMethod_Validation() + { + var type = typeof(string); + var types = new[] {typeof(int)}; + + // Negative genericParameterCount throws ArgumentException; the exact subtype varies by + // framework (ArgumentException on net6-8, ArgumentOutOfRangeException on net9+/the polyfill). + ArgumentException? genericCountError = null; + try + { + type.GetMethod("Substring", -1, BindingFlags.Public | BindingFlags.Instance, null, types, null); + } + catch (ArgumentException exception) + { + genericCountError = exception; + } + + await Assert.That(genericCountError).IsNotNull(); + await Assert.That(genericCountError!.ParamName).IsEqualTo("genericParameterCount"); + + await Assert.That(() => type.GetMethod(null!, 0, BindingFlags.Public, null, types, null)).Throws(); + await Assert.That(() => type.GetMethod("Substring", 0, BindingFlags.Public, null, null!, null)).Throws(); + } + + [Test] + public async Task MemberInfo_HasSameMetadataDefinitionAs_Null_Throws() + { + var member = typeof(string).GetMethod("Trim", Type.EmptyTypes)!; + await Assert.That(() => member.HasSameMetadataDefinitionAs(null!)).Throws(); + } + + [Test] + public async Task TaskCompletionSource_TrySetCanceled_PropagatesToken() + { + using var source = new CancellationTokenSource(); + source.Cancel(); + var token = source.Token; + + var completionSource = new TaskCompletionSource(); + var result = completionSource.TrySetCanceled(token); + + await Assert.That(result).IsTrue(); + await Assert.That(completionSource.Task.IsCanceled).IsTrue(); + + CancellationToken observed = default; + try + { + await completionSource.Task; + } + catch (OperationCanceledException exception) + { + observed = exception.CancellationToken; + } + + await Assert.That(observed).IsEqualTo(token); + } + + [Test] + public async Task FileUnixMode_SetUser_RoundTrip() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // GetUnixFileMode/SetUnixFileMode are not supported on Windows. + return; + } + + var path = Path.GetTempFileName(); + try + { + File.SetUnixFileMode(path, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.SetUser); + var mode = File.GetUnixFileMode(path); + + // setuid is octal 4 (SetUser); it must not be read/written as setgid (octal 2). + await Assert.That(mode.HasFlag(UnixFileMode.SetUser)).IsTrue(); + await Assert.That(mode.HasFlag(UnixFileMode.SetGroup)).IsFalse(); + } + finally + { + File.Delete(path); + } + } + + [Test] + public async Task XDocument_And_XElement_LoadAsync_HonorDeclaredEncoding() + { + var accentedE = (char) 0x00E9; + var xml = $"caf{accentedE}"; + var expected = $"caf{accentedE}"; + // UTF-16 LE, no BOM: a UTF-8 StreamReader would mis-decode this. + var bytes = Encoding.Unicode.GetBytes(xml); + + using var documentStream = new MemoryStream(bytes); + var document = await XDocument.LoadAsync(documentStream, LoadOptions.None, CancellationToken.None); + await Assert.That(document.Root!.Value).IsEqualTo(expected); + + using var elementStream = new MemoryStream(bytes); + var element = await XElement.LoadAsync(elementStream, LoadOptions.None, CancellationToken.None); + await Assert.That(element.Value).IsEqualTo(expected); + } + + [Test] + public async Task ZipFile_ExtractToDirectory_Overwrite_PreservesExistingFiles() + { + var root = Path.Combine(Path.GetTempPath(), "polyfill_zip_" + Guid.NewGuid().ToString("N")); + var zipPath = Path.Combine(root, "archive.zip"); + var destination = Path.Combine(root, "dest"); + Directory.CreateDirectory(root); + Directory.CreateDirectory(destination); + try + { + using (var archive = ZipFile.Open(zipPath, ZipArchiveMode.Create)) + using (var writer = new StreamWriter(archive.CreateEntry("a.txt").Open())) + { + writer.Write("from-archive"); + } + + File.WriteAllText(Path.Combine(destination, "preexisting.txt"), "keep-me"); + File.WriteAllText(Path.Combine(destination, "a.txt"), "old"); + + await ZipFile.ExtractToDirectoryAsync(zipPath, destination, overwriteFiles: true); + + // Pre-existing file not in the archive is preserved; archive file is overwritten. + await Assert.That(File.Exists(Path.Combine(destination, "preexisting.txt"))).IsTrue(); + await Assert.That(File.ReadAllText(Path.Combine(destination, "preexisting.txt"))).IsEqualTo("keep-me"); + await Assert.That(File.ReadAllText(Path.Combine(destination, "a.txt"))).IsEqualTo("from-archive"); + } + finally + { + Directory.Delete(root, true); + } + } + + [Test] + public async Task RandomNumberGenerator_GetInt32_ReturnsFullRange() + { + var seen = new HashSet(); + for (var i = 0; i < 200; i++) + { + seen.Add(RandomNumberGenerator.GetInt32(0, 3)); + } + + await Assert.That(seen.Contains(0)).IsTrue(); + await Assert.That(seen.Contains(1)).IsTrue(); + // The exclusive-upper-bound-minus-one value was previously never produced. + await Assert.That(seen.Contains(2)).IsTrue(); + await Assert.That(seen.All(_ => _ is >= 0 and < 3)).IsTrue(); + } + + [Test] + public async Task Convert_FromHexString_ConsumedOnInvalidData() + { + var destination = new byte[8]; + + // A valid leading nibble is counted as consumed; consumed stops at i unless the low nibble is valid. + await AssertConsumed("4G", 1); // hi valid, lo invalid + await AssertConsumed("G4", 0); // hi invalid, lo valid + await AssertConsumed("GG", 1); // both invalid + await AssertConsumed("444G", 3); // failure mid-span + + // The UTF-8 byte overload shares the same consumed semantics. + var bytesStatus = Convert.FromHexString("4G"u8, destination, out var bytesConsumed, out _); + await Assert.That(bytesStatus).IsEqualTo(OperationStatus.InvalidData); + await Assert.That(bytesConsumed).IsEqualTo(1); + + // DestinationTooSmall takes priority over InvalidData when the buffer is already full. + var smallDestination = new byte[2]; + var fullStatus = Convert.FromHexString("a1F6CG".AsSpan(), smallDestination, out var fullConsumed, out var fullWritten); + await Assert.That(fullStatus).IsEqualTo(OperationStatus.DestinationTooSmall); + await Assert.That(fullConsumed).IsEqualTo(4); + await Assert.That(fullWritten).IsEqualTo(2); + + async Task AssertConsumed(string source, int expectedConsumed) + { + var status = Convert.FromHexString(source.AsSpan(), destination, out var consumed, out _); + await Assert.That(status).IsEqualTo(OperationStatus.InvalidData); + await Assert.That(consumed).IsEqualTo(expectedConsumed); + } + } + + [Test] + public async Task ArgumentOutOfRangeException_ThrowIfZero_SetsActualValueAndParamName() + { + ArgumentOutOfRangeException? captured = null; + try + { + ArgumentOutOfRangeException.ThrowIfZero(0, "count"); + } + catch (ArgumentOutOfRangeException exception) + { + captured = exception; + } + + await Assert.That(captured).IsNotNull(); + await Assert.That(captured!.ActualValue).IsEqualTo((object) 0); + await Assert.That(captured.ParamName).IsEqualTo("count"); + } +} diff --git a/src/Tests/PolyfillTests_String.cs b/src/Tests/PolyfillTests_String.cs index 0744f574e..42cba485b 100644 --- a/src/Tests/PolyfillTests_String.cs +++ b/src/Tests/PolyfillTests_String.cs @@ -30,6 +30,23 @@ public async Task ReplaceLineEndings() } await Assert.That("a\rb\nc\r\nd".ReplaceLineEndings("_")).IsEqualTo("a_b_c_d"); + + // empty input must not throw + await Assert.That("".ReplaceLineEndings("_")).IsEqualTo(""); + // a trailing line ending must be preserved + await Assert.That("a\n".ReplaceLineEndings("_")).IsEqualTo("a_"); + await Assert.That("\n".ReplaceLineEndings("_")).IsEqualTo("_"); + // consecutive line endings each get replaced + await Assert.That("a\n\nb".ReplaceLineEndings("_")).IsEqualTo("a__b"); + // a string with no line endings is returned unchanged + await Assert.That("abc".ReplaceLineEndings("_")).IsEqualTo("abc"); + // the full BCL line-ending set: FF (U+000C), NEL (U+0085), LS (U+2028), PS (U+2029) + var formFeed = (char) 0x000C; + var nextLine = (char) 0x0085; + var lineSeparator = (char) 0x2028; + var paragraphSeparator = (char) 0x2029; + var allSeparators = $"a{formFeed}b{nextLine}c{lineSeparator}d{paragraphSeparator}e"; + await Assert.That(allSeparators.ReplaceLineEndings("_")).IsEqualTo("a_b_c_d_e"); } [Test] diff --git a/src/Tests/PolyfillTests_StringBuilder.cs b/src/Tests/PolyfillTests_StringBuilder.cs index ad42616d9..0ed518039 100644 --- a/src/Tests/PolyfillTests_StringBuilder.cs +++ b/src/Tests/PolyfillTests_StringBuilder.cs @@ -22,6 +22,21 @@ public async Task StringBuilderCopyTo() await Assert.That(span is "value").IsTrue(); } + [Test] + public async Task StringBuilderCopyTo_InvalidArgs() + { + var builder = new StringBuilder("value"); + + // count < 0 + await Assert.That(() => builder.CopyTo(0, new Span(new char[2]), -1)).Throws(); + // sourceIndex past the end + await Assert.That(() => builder.CopyTo(6, new Span(new char[2]), 0)).Throws(); + // not enough source characters from sourceIndex onward (silent truncation previously) + await Assert.That(() => builder.CopyTo(3, new Span(new char[5]), 5)).Throws(); + // destination too short + await Assert.That(() => builder.CopyTo(0, new Span(new char[2]), 5)).Throws(); + } + [Test] public async Task Replace() { @@ -130,6 +145,18 @@ public async Task StringBuilder_Append_StringBuilder() await Assert.That(target.ToString()).IsEqualTo("ell"); } + [Test] + public async Task StringBuilder_Append_StringBuilder_InvalidArgs() + { + var value = new StringBuilder("world"); + + // negative startIndex/count throw even when count is 0 + await Assert.That(() => new StringBuilder().Append(value, -1, 0)).Throws(); + await Assert.That(() => new StringBuilder().Append(value, 0, -1)).Throws(); + // count == 0 with a non-negative startIndex is a no-op (matches the BCL) + await Assert.That(new StringBuilder().Append(value, 999, 0).ToString()).IsEqualTo(""); + } + [Test] public async Task AppendJoin() { diff --git a/src/Tests/StringPolyfillTests.cs b/src/Tests/StringPolyfillTests.cs index f19f58ffa..5cd090d72 100644 --- a/src/Tests/StringPolyfillTests.cs +++ b/src/Tests/StringPolyfillTests.cs @@ -7,6 +7,8 @@ public class StringPolyfillTest public async Task Join() { await Assert.That(string.Join('a', ["b", "c"])).IsEqualTo("bac"); + // null element with a char separator binds to the ReadOnlySpan overload; null is treated as empty + await Assert.That(string.Join('a', ["b", null, "c"])).IsEqualTo("baac"); await Assert.That(string.Join("a1", ["b", "c"])).IsEqualTo("ba1c"); await Assert.That(string.Join("a1", ["b", null, "c"])).IsEqualTo("ba1a1c"); await Assert.That(string.Join('a', new object[] {"b", "c"})).IsEqualTo("bac"); From efff36d3006bb3e3bc155f46833181771f738cdf Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Tue, 30 Jun 2026 11:23:39 +1000 Subject: [PATCH 2/2] . --- .../ArgumentOutOfRangeExceptionTests.cs | 18 ++ src/Tests/ConvertPolyfillTests.cs | 33 ++ src/Tests/EqualityComparerPolyfillTests.cs | 15 + src/Tests/FilePolyfillTests.cs | 25 ++ src/Tests/Numbers/DoublePolyfillTests.cs | 12 + src/Tests/Numbers/SinglePolyfillTests.cs | 12 + src/Tests/PolyfillTests_ArraySegment.cs | 8 + src/Tests/PolyfillTests_Compression.cs | 32 ++ .../PolyfillTests_ConcurrentDictionary.cs | 12 + src/Tests/PolyfillTests_List.cs | 21 ++ src/Tests/PolyfillTests_Pass2.cs | 299 ------------------ .../PolyfillTests_TaskCompletionSource.cs | 26 ++ src/Tests/PolyfillTests_Type.cs | 32 ++ src/Tests/PolyfillTests_XDocument.cs | 13 + src/Tests/PolyfillTests_XElement.cs | 13 + .../RandomNumberGeneratorPolyfillTests.cs | 16 + src/Tests/TimeSpanPolyfillTests.cs | 19 ++ 17 files changed, 307 insertions(+), 299 deletions(-) delete mode 100644 src/Tests/PolyfillTests_Pass2.cs diff --git a/src/Tests/ArgumentExceptions/ArgumentOutOfRangeExceptionTests.cs b/src/Tests/ArgumentExceptions/ArgumentOutOfRangeExceptionTests.cs index fed0f2f5f..8ae0cfde1 100644 --- a/src/Tests/ArgumentExceptions/ArgumentOutOfRangeExceptionTests.cs +++ b/src/Tests/ArgumentExceptions/ArgumentOutOfRangeExceptionTests.cs @@ -4,6 +4,24 @@ public class ArgumentOutOfRangeExceptionTests { #region ThrowIfZero Tests + [Test] + public async Task ThrowIfZero_SetsActualValueAndParamName() + { + ArgumentOutOfRangeException? captured = null; + try + { + ArgumentOutOfRangeException.ThrowIfZero(0, "count"); + } + catch (ArgumentOutOfRangeException exception) + { + captured = exception; + } + + await Assert.That(captured).IsNotNull(); + await Assert.That(captured!.ActualValue).IsEqualTo((object) 0); + await Assert.That(captured.ParamName).IsEqualTo("count"); + } + [Test] public async Task ThrowIfZero_WithZeroInt_ThrowsArgumentOutOfRangeException() { diff --git a/src/Tests/ConvertPolyfillTests.cs b/src/Tests/ConvertPolyfillTests.cs index 55eece71c..54f78edda 100644 --- a/src/Tests/ConvertPolyfillTests.cs +++ b/src/Tests/ConvertPolyfillTests.cs @@ -1,5 +1,38 @@ +using System.Buffers; + public class ConvertPolyfillTests { + [Test] + public async Task FromHexString_ConsumedOnInvalidData() + { + var destination = new byte[8]; + + // A valid leading nibble is counted as consumed; consumed stops at i unless the low nibble is valid. + await AssertConsumed("4G", 1); // hi valid, lo invalid + await AssertConsumed("G4", 0); // hi invalid, lo valid + await AssertConsumed("GG", 1); // both invalid + await AssertConsumed("444G", 3); // failure mid-span + + // The UTF-8 byte overload shares the same consumed semantics. + var bytesStatus = Convert.FromHexString("4G"u8, destination, out var bytesConsumed, out _); + await Assert.That(bytesStatus).IsEqualTo(OperationStatus.InvalidData); + await Assert.That(bytesConsumed).IsEqualTo(1); + + // DestinationTooSmall takes priority over InvalidData when the buffer is already full. + var smallDestination = new byte[2]; + var fullStatus = Convert.FromHexString("a1F6CG".AsSpan(), smallDestination, out var fullConsumed, out var fullWritten); + await Assert.That(fullStatus).IsEqualTo(OperationStatus.DestinationTooSmall); + await Assert.That(fullConsumed).IsEqualTo(4); + await Assert.That(fullWritten).IsEqualTo(2); + + async Task AssertConsumed(string source, int expectedConsumed) + { + var status = Convert.FromHexString(source.AsSpan(), destination, out var consumed, out _); + await Assert.That(status).IsEqualTo(OperationStatus.InvalidData); + await Assert.That(consumed).IsEqualTo(expectedConsumed); + } + } + [Test] public async Task ToHexString_ValidInput() { diff --git a/src/Tests/EqualityComparerPolyfillTests.cs b/src/Tests/EqualityComparerPolyfillTests.cs index 74e8f0c76..bb4cb1795 100644 --- a/src/Tests/EqualityComparerPolyfillTests.cs +++ b/src/Tests/EqualityComparerPolyfillTests.cs @@ -3,6 +3,21 @@ public class EqualityComparerPolyfillTests { + [Test] + public async Task Create_NullDelegates_Throw() + { + await Assert.That(() => EqualityComparer.Create(null!)).Throws(); + + // The keySelector overload's delegate signature differs in nullability between the + // polyfill (Func) and the .NET 11 BCL (Func); adapt accordingly. +#if NET11_0_OR_GREATER + Func keySelector = null!; +#else + Func keySelector = null!; +#endif + await Assert.That(() => EqualityComparer.Create(keySelector)).Throws(); + } + [Test] public async Task Create_WithEqualsAndGetHashCode() { diff --git a/src/Tests/FilePolyfillTests.cs b/src/Tests/FilePolyfillTests.cs index 24a0aa7a2..fd8387bad 100644 --- a/src/Tests/FilePolyfillTests.cs +++ b/src/Tests/FilePolyfillTests.cs @@ -27,6 +27,31 @@ public void TearDown() File.Delete(hardLinkFilePath); } + [Test] + public async Task GetSetUnixFileMode_SetUser_RoundTrip() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // GetUnixFileMode/SetUnixFileMode are not supported on Windows. + return; + } + + var path = Path.GetTempFileName(); + try + { + File.SetUnixFileMode(path, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.SetUser); + var mode = File.GetUnixFileMode(path); + + // setuid is octal 4 (SetUser); it must not be read/written as setgid (octal 2). + await Assert.That(mode.HasFlag(UnixFileMode.SetUser)).IsTrue(); + await Assert.That(mode.HasFlag(UnixFileMode.SetGroup)).IsFalse(); + } + finally + { + File.Delete(path); + } + } + #if FeatureMemory [Test] public async Task AppendAllBytes() diff --git a/src/Tests/Numbers/DoublePolyfillTests.cs b/src/Tests/Numbers/DoublePolyfillTests.cs index efa630762..eaf644248 100644 --- a/src/Tests/Numbers/DoublePolyfillTests.cs +++ b/src/Tests/Numbers/DoublePolyfillTests.cs @@ -1,5 +1,17 @@ public class DoublePolyfillTest { + [Test] + public async Task TryParse_AllowsThousands() + { + var invariant = CultureInfo.InvariantCulture; + + await Assert.That(double.TryParse("1,234.5", invariant, out var value)).IsTrue(); + await Assert.That(value).IsEqualTo(1234.5); + + await Assert.That(double.TryParse("1,234.5".AsSpan(), invariant, out var spanValue)).IsTrue(); + await Assert.That(spanValue).IsEqualTo(1234.5); + } + [Test] public async Task TryParse() { diff --git a/src/Tests/Numbers/SinglePolyfillTests.cs b/src/Tests/Numbers/SinglePolyfillTests.cs index 74f3fe03d..81f13ad08 100644 --- a/src/Tests/Numbers/SinglePolyfillTests.cs +++ b/src/Tests/Numbers/SinglePolyfillTests.cs @@ -1,5 +1,17 @@ public class SinglePolyfillTest { + [Test] + public async Task TryParse_AllowsThousands() + { + var invariant = CultureInfo.InvariantCulture; + + await Assert.That(float.TryParse("1,234.5", invariant, out var value)).IsTrue(); + await Assert.That(value).IsEqualTo(1234.5f); + + await Assert.That(float.TryParse("1,234.5".AsSpan(), invariant, out var spanValue)).IsTrue(); + await Assert.That(spanValue).IsEqualTo(1234.5f); + } + [Test] public async Task TryParse() { diff --git a/src/Tests/PolyfillTests_ArraySegment.cs b/src/Tests/PolyfillTests_ArraySegment.cs index 457329e5b..ae8903c52 100644 --- a/src/Tests/PolyfillTests_ArraySegment.cs +++ b/src/Tests/PolyfillTests_ArraySegment.cs @@ -1,6 +1,14 @@ partial class PolyfillTests { + [Test] + public async Task ArraySegment_CopyTo_DefaultDestination_Throws() + { + var source = new ArraySegment(new[] {1, 2, 3}); + ArraySegment destination = default; + await Assert.That(() => source.CopyTo(destination)).Throws(); + } + [Test] public async Task CopyTo_ArraySegment_CopiesElements() { diff --git a/src/Tests/PolyfillTests_Compression.cs b/src/Tests/PolyfillTests_Compression.cs index fc8b7d52f..1b1fae259 100644 --- a/src/Tests/PolyfillTests_Compression.cs +++ b/src/Tests/PolyfillTests_Compression.cs @@ -3,6 +3,38 @@ partial class PolyfillTests { + [Test] + public async Task ZipFile_ExtractToDirectory_Overwrite_PreservesExistingFiles() + { + var root = Path.Combine(Path.GetTempPath(), "polyfill_zip_" + Guid.NewGuid().ToString("N")); + var zipPath = Path.Combine(root, "archive.zip"); + var destination = Path.Combine(root, "dest"); + Directory.CreateDirectory(root); + Directory.CreateDirectory(destination); + try + { + using (var archive = ZipFile.Open(zipPath, ZipArchiveMode.Create)) + using (var writer = new StreamWriter(archive.CreateEntry("a.txt").Open())) + { + writer.Write("from-archive"); + } + + File.WriteAllText(Path.Combine(destination, "preexisting.txt"), "keep-me"); + File.WriteAllText(Path.Combine(destination, "a.txt"), "old"); + + await ZipFile.ExtractToDirectoryAsync(zipPath, destination, overwriteFiles: true); + + // Pre-existing file not in the archive is preserved; archive file is overwritten. + await Assert.That(File.Exists(Path.Combine(destination, "preexisting.txt"))).IsTrue(); + await Assert.That(File.ReadAllText(Path.Combine(destination, "preexisting.txt"))).IsEqualTo("keep-me"); + await Assert.That(File.ReadAllText(Path.Combine(destination, "a.txt"))).IsEqualTo("from-archive"); + } + finally + { + Directory.Delete(root, true); + } + } + [Test] public async Task ZipArchiveEntry_Open_WithFileAccess() { diff --git a/src/Tests/PolyfillTests_ConcurrentDictionary.cs b/src/Tests/PolyfillTests_ConcurrentDictionary.cs index cc304660f..2cf86f9ae 100644 --- a/src/Tests/PolyfillTests_ConcurrentDictionary.cs +++ b/src/Tests/PolyfillTests_ConcurrentDictionary.cs @@ -1,5 +1,17 @@ partial class PolyfillTests { + [Test] + public async Task ConcurrentDictionary_GetOrAdd_NullFactory_Throws() + { + var dictionary = new ConcurrentDictionary(); + dictionary.TryAdd("a", 1); + Func factory = null!; + + // Eager validation: throws for both present and absent keys. + await Assert.That(() => dictionary.GetOrAdd("a", factory, 5)).Throws(); + await Assert.That(() => dictionary.GetOrAdd("b", factory, 5)).Throws(); + } + [Test] public async Task ConcurrentDictionaryGetOrAddFunc() { diff --git a/src/Tests/PolyfillTests_List.cs b/src/Tests/PolyfillTests_List.cs index ce4c1cd8c..29beb6671 100644 --- a/src/Tests/PolyfillTests_List.cs +++ b/src/Tests/PolyfillTests_List.cs @@ -1,5 +1,26 @@ partial class PolyfillTests { + [Test] + public async Task List_CopyTo_TooShort_Throws_AndLeavesDestinationUntouched() + { + var list = new List {10, 20, 30, 40, 50}; + + var destination = new int[3]; + await Assert.That(() => list.CopyTo(new Span(destination))).Throws(); + await Assert.That(destination.All(_ => _ == 0)).IsTrue(); + + var ok = new int[5]; + list.CopyTo(new Span(ok)); + await Assert.That(string.Join(",", ok)).IsEqualTo("10,20,30,40,50"); + } + + [Test] + public async Task List_InsertRange_InvalidIndex_Throws() + { + var list = new List {1, 2, 3}; + await Assert.That(() => list.InsertRange(10, ReadOnlySpan.Empty)).Throws(); + } + [Test] public async Task IListAsReadOnly() { diff --git a/src/Tests/PolyfillTests_Pass2.cs b/src/Tests/PolyfillTests_Pass2.cs deleted file mode 100644 index d90f96705..000000000 --- a/src/Tests/PolyfillTests_Pass2.cs +++ /dev/null @@ -1,299 +0,0 @@ -using System.Buffers; -using System.IO.Compression; -using System.Security.Cryptography; - -// Regression tests for the second-pass bug fixes. Each runs on every target framework, -// exercising the polyfill on older TFMs and the BCL on newer ones. -partial class PolyfillTests -{ - [Test] - public async Task ConcurrentDictionary_GetOrAdd_NullFactory_Throws() - { - var dictionary = new ConcurrentDictionary(); - dictionary.TryAdd("a", 1); - Func factory = null!; - - // Eager validation: throws for both present and absent keys. - await Assert.That(() => dictionary.GetOrAdd("a", factory, 5)).Throws(); - await Assert.That(() => dictionary.GetOrAdd("b", factory, 5)).Throws(); - } - - [Test] - public async Task List_CopyTo_TooShort_Throws_AndLeavesDestinationUntouched() - { - var list = new List {10, 20, 30, 40, 50}; - - var destination = new int[3]; - await Assert.That(() => list.CopyTo(new Span(destination))).Throws(); - await Assert.That(destination.All(_ => _ == 0)).IsTrue(); - - var ok = new int[5]; - list.CopyTo(new Span(ok)); - await Assert.That(string.Join(",", ok)).IsEqualTo("10,20,30,40,50"); - } - - [Test] - public async Task List_InsertRange_InvalidIndex_Throws() - { - var list = new List {1, 2, 3}; - await Assert.That(() => list.InsertRange(10, ReadOnlySpan.Empty)).Throws(); - } - - [Test] - public async Task EqualityComparer_Create_NullDelegates_Throw() - { - await Assert.That(() => EqualityComparer.Create(null!)).Throws(); - - // The keySelector overload's delegate signature differs in nullability between the - // polyfill (Func) and the .NET 11 BCL (Func); adapt accordingly. -#if NET11_0_OR_GREATER - Func keySelector = null!; -#else - Func keySelector = null!; -#endif - await Assert.That(() => EqualityComparer.Create(keySelector)).Throws(); - } - - [Test] - public async Task FloatingPoint_TryParse_AllowsThousands() - { - var invariant = CultureInfo.InvariantCulture; - - await Assert.That(double.TryParse("1,234.5", invariant, out var d)).IsTrue(); - await Assert.That(d).IsEqualTo(1234.5); - - await Assert.That(double.TryParse("1,234.5".AsSpan(), invariant, out var d2)).IsTrue(); - await Assert.That(d2).IsEqualTo(1234.5); - - await Assert.That(float.TryParse("1,234.5", invariant, out var f)).IsTrue(); - await Assert.That(f).IsEqualTo(1234.5f); - - await Assert.That(float.TryParse("1,234.5".AsSpan(), invariant, out var f2)).IsTrue(); - await Assert.That(f2).IsEqualTo(1234.5f); - } - - [Test] - public async Task ArraySegment_CopyTo_DefaultDestination_Throws() - { - var source = new ArraySegment(new[] {1, 2, 3}); - ArraySegment destination = default; - await Assert.That(() => source.CopyTo(destination)).Throws(); - } - - [Test] - public async Task TimeSpan_From_HappyPath_And_Overflow() - { - await Assert.That(TimeSpan.FromMicroseconds(1000000).Ticks).IsEqualTo(TimeSpan.TicksPerSecond); - await Assert.That(TimeSpan.FromDays(1).Ticks).IsEqualTo(TimeSpan.TicksPerDay); - await Assert.That(TimeSpan.FromSeconds(1, 0, 0).Ticks).IsEqualTo(TimeSpan.TicksPerSecond); - - // Components that cancel out must stay in range. Accumulation happens in a wide (decimal) - // space, so even when an individual component product overflows Int64, a cancelling - // component keeps the result in range (matching the BCL's 128-bit accumulation). - await Assert.That(TimeSpan.FromSeconds(1000000000000, -1000000000000000, 0).Ticks).IsEqualTo(0L); - await Assert.That(TimeSpan.FromSeconds(0, -9498038399810654, long.MaxValue).Ticks).IsEqualTo(-2746663629558781930L); - - // The multi-argument overloads reach the integer-based factory on every framework - // (the single-argument FromMicroseconds(double) BCL overload shadows the polyfill on net7/8). - await Assert.That(() => TimeSpan.FromDays(int.MaxValue, 0, 0, 0, 0, 0)).Throws(); - await Assert.That(() => TimeSpan.FromSeconds(long.MaxValue, 0, 0)).Throws(); - } - - [Test] - public async Task Type_GetMethod_Validation() - { - var type = typeof(string); - var types = new[] {typeof(int)}; - - // Negative genericParameterCount throws ArgumentException; the exact subtype varies by - // framework (ArgumentException on net6-8, ArgumentOutOfRangeException on net9+/the polyfill). - ArgumentException? genericCountError = null; - try - { - type.GetMethod("Substring", -1, BindingFlags.Public | BindingFlags.Instance, null, types, null); - } - catch (ArgumentException exception) - { - genericCountError = exception; - } - - await Assert.That(genericCountError).IsNotNull(); - await Assert.That(genericCountError!.ParamName).IsEqualTo("genericParameterCount"); - - await Assert.That(() => type.GetMethod(null!, 0, BindingFlags.Public, null, types, null)).Throws(); - await Assert.That(() => type.GetMethod("Substring", 0, BindingFlags.Public, null, null!, null)).Throws(); - } - - [Test] - public async Task MemberInfo_HasSameMetadataDefinitionAs_Null_Throws() - { - var member = typeof(string).GetMethod("Trim", Type.EmptyTypes)!; - await Assert.That(() => member.HasSameMetadataDefinitionAs(null!)).Throws(); - } - - [Test] - public async Task TaskCompletionSource_TrySetCanceled_PropagatesToken() - { - using var source = new CancellationTokenSource(); - source.Cancel(); - var token = source.Token; - - var completionSource = new TaskCompletionSource(); - var result = completionSource.TrySetCanceled(token); - - await Assert.That(result).IsTrue(); - await Assert.That(completionSource.Task.IsCanceled).IsTrue(); - - CancellationToken observed = default; - try - { - await completionSource.Task; - } - catch (OperationCanceledException exception) - { - observed = exception.CancellationToken; - } - - await Assert.That(observed).IsEqualTo(token); - } - - [Test] - public async Task FileUnixMode_SetUser_RoundTrip() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - // GetUnixFileMode/SetUnixFileMode are not supported on Windows. - return; - } - - var path = Path.GetTempFileName(); - try - { - File.SetUnixFileMode(path, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.SetUser); - var mode = File.GetUnixFileMode(path); - - // setuid is octal 4 (SetUser); it must not be read/written as setgid (octal 2). - await Assert.That(mode.HasFlag(UnixFileMode.SetUser)).IsTrue(); - await Assert.That(mode.HasFlag(UnixFileMode.SetGroup)).IsFalse(); - } - finally - { - File.Delete(path); - } - } - - [Test] - public async Task XDocument_And_XElement_LoadAsync_HonorDeclaredEncoding() - { - var accentedE = (char) 0x00E9; - var xml = $"caf{accentedE}"; - var expected = $"caf{accentedE}"; - // UTF-16 LE, no BOM: a UTF-8 StreamReader would mis-decode this. - var bytes = Encoding.Unicode.GetBytes(xml); - - using var documentStream = new MemoryStream(bytes); - var document = await XDocument.LoadAsync(documentStream, LoadOptions.None, CancellationToken.None); - await Assert.That(document.Root!.Value).IsEqualTo(expected); - - using var elementStream = new MemoryStream(bytes); - var element = await XElement.LoadAsync(elementStream, LoadOptions.None, CancellationToken.None); - await Assert.That(element.Value).IsEqualTo(expected); - } - - [Test] - public async Task ZipFile_ExtractToDirectory_Overwrite_PreservesExistingFiles() - { - var root = Path.Combine(Path.GetTempPath(), "polyfill_zip_" + Guid.NewGuid().ToString("N")); - var zipPath = Path.Combine(root, "archive.zip"); - var destination = Path.Combine(root, "dest"); - Directory.CreateDirectory(root); - Directory.CreateDirectory(destination); - try - { - using (var archive = ZipFile.Open(zipPath, ZipArchiveMode.Create)) - using (var writer = new StreamWriter(archive.CreateEntry("a.txt").Open())) - { - writer.Write("from-archive"); - } - - File.WriteAllText(Path.Combine(destination, "preexisting.txt"), "keep-me"); - File.WriteAllText(Path.Combine(destination, "a.txt"), "old"); - - await ZipFile.ExtractToDirectoryAsync(zipPath, destination, overwriteFiles: true); - - // Pre-existing file not in the archive is preserved; archive file is overwritten. - await Assert.That(File.Exists(Path.Combine(destination, "preexisting.txt"))).IsTrue(); - await Assert.That(File.ReadAllText(Path.Combine(destination, "preexisting.txt"))).IsEqualTo("keep-me"); - await Assert.That(File.ReadAllText(Path.Combine(destination, "a.txt"))).IsEqualTo("from-archive"); - } - finally - { - Directory.Delete(root, true); - } - } - - [Test] - public async Task RandomNumberGenerator_GetInt32_ReturnsFullRange() - { - var seen = new HashSet(); - for (var i = 0; i < 200; i++) - { - seen.Add(RandomNumberGenerator.GetInt32(0, 3)); - } - - await Assert.That(seen.Contains(0)).IsTrue(); - await Assert.That(seen.Contains(1)).IsTrue(); - // The exclusive-upper-bound-minus-one value was previously never produced. - await Assert.That(seen.Contains(2)).IsTrue(); - await Assert.That(seen.All(_ => _ is >= 0 and < 3)).IsTrue(); - } - - [Test] - public async Task Convert_FromHexString_ConsumedOnInvalidData() - { - var destination = new byte[8]; - - // A valid leading nibble is counted as consumed; consumed stops at i unless the low nibble is valid. - await AssertConsumed("4G", 1); // hi valid, lo invalid - await AssertConsumed("G4", 0); // hi invalid, lo valid - await AssertConsumed("GG", 1); // both invalid - await AssertConsumed("444G", 3); // failure mid-span - - // The UTF-8 byte overload shares the same consumed semantics. - var bytesStatus = Convert.FromHexString("4G"u8, destination, out var bytesConsumed, out _); - await Assert.That(bytesStatus).IsEqualTo(OperationStatus.InvalidData); - await Assert.That(bytesConsumed).IsEqualTo(1); - - // DestinationTooSmall takes priority over InvalidData when the buffer is already full. - var smallDestination = new byte[2]; - var fullStatus = Convert.FromHexString("a1F6CG".AsSpan(), smallDestination, out var fullConsumed, out var fullWritten); - await Assert.That(fullStatus).IsEqualTo(OperationStatus.DestinationTooSmall); - await Assert.That(fullConsumed).IsEqualTo(4); - await Assert.That(fullWritten).IsEqualTo(2); - - async Task AssertConsumed(string source, int expectedConsumed) - { - var status = Convert.FromHexString(source.AsSpan(), destination, out var consumed, out _); - await Assert.That(status).IsEqualTo(OperationStatus.InvalidData); - await Assert.That(consumed).IsEqualTo(expectedConsumed); - } - } - - [Test] - public async Task ArgumentOutOfRangeException_ThrowIfZero_SetsActualValueAndParamName() - { - ArgumentOutOfRangeException? captured = null; - try - { - ArgumentOutOfRangeException.ThrowIfZero(0, "count"); - } - catch (ArgumentOutOfRangeException exception) - { - captured = exception; - } - - await Assert.That(captured).IsNotNull(); - await Assert.That(captured!.ActualValue).IsEqualTo((object) 0); - await Assert.That(captured.ParamName).IsEqualTo("count"); - } -} diff --git a/src/Tests/PolyfillTests_TaskCompletionSource.cs b/src/Tests/PolyfillTests_TaskCompletionSource.cs index 294b26fc3..4c97611f3 100644 --- a/src/Tests/PolyfillTests_TaskCompletionSource.cs +++ b/src/Tests/PolyfillTests_TaskCompletionSource.cs @@ -1,6 +1,32 @@ #pragma warning disable CS4014 partial class PolyfillTests { + [Test] + public async Task TaskCompletionSource_TrySetCanceled_PropagatesToken() + { + using var source = new CancellationTokenSource(); + source.Cancel(); + var token = source.Token; + + var completionSource = new TaskCompletionSource(); + var result = completionSource.TrySetCanceled(token); + + await Assert.That(result).IsTrue(); + await Assert.That(completionSource.Task.IsCanceled).IsTrue(); + + CancellationToken observed = default; + try + { + await completionSource.Task; + } + catch (OperationCanceledException exception) + { + observed = exception.CancellationToken; + } + + await Assert.That(observed).IsEqualTo(token); + } + [Test] public async Task TaskCompletionSource() { diff --git a/src/Tests/PolyfillTests_Type.cs b/src/Tests/PolyfillTests_Type.cs index 41fdf5837..995a1ac48 100644 --- a/src/Tests/PolyfillTests_Type.cs +++ b/src/Tests/PolyfillTests_Type.cs @@ -1,5 +1,37 @@ partial class PolyfillTests { + [Test] + public async Task Type_GetMethod_Validation() + { + var type = typeof(string); + var types = new[] {typeof(int)}; + + // Negative genericParameterCount throws ArgumentException; the exact subtype varies by + // framework (ArgumentException on net6-8, ArgumentOutOfRangeException on net9+/the polyfill). + ArgumentException? genericCountError = null; + try + { + type.GetMethod("Substring", -1, BindingFlags.Public | BindingFlags.Instance, null, types, null); + } + catch (ArgumentException exception) + { + genericCountError = exception; + } + + await Assert.That(genericCountError).IsNotNull(); + await Assert.That(genericCountError!.ParamName).IsEqualTo("genericParameterCount"); + + await Assert.That(() => type.GetMethod(null!, 0, BindingFlags.Public, null, types, null)).Throws(); + await Assert.That(() => type.GetMethod("Substring", 0, BindingFlags.Public, null, null!, null)).Throws(); + } + + [Test] + public async Task MemberInfo_HasSameMetadataDefinitionAs_Null_Throws() + { + var member = typeof(string).GetMethod("Trim", Type.EmptyTypes)!; + await Assert.That(() => member.HasSameMetadataDefinitionAs(null!)).Throws(); + } + [Test] public async Task HasSameMetadataDefinitionAs() { diff --git a/src/Tests/PolyfillTests_XDocument.cs b/src/Tests/PolyfillTests_XDocument.cs index e75687096..75c8c1c00 100644 --- a/src/Tests/PolyfillTests_XDocument.cs +++ b/src/Tests/PolyfillTests_XDocument.cs @@ -1,5 +1,18 @@ partial class PolyfillTests { + [Test] + public async Task XDocument_LoadAsync_HonorsDeclaredEncoding() + { + var accentedE = (char) 0x00E9; + var xml = $"caf{accentedE}"; + // UTF-16 LE, no BOM: a UTF-8 StreamReader would mis-decode this. + var bytes = Encoding.Unicode.GetBytes(xml); + + using var stream = new MemoryStream(bytes); + var document = await XDocument.LoadAsync(stream, LoadOptions.None, CancellationToken.None); + await Assert.That(document.Root!.Value).IsEqualTo($"caf{accentedE}"); + } + [Test] public async Task XDocumentSaveAsyncXmlWriter() { diff --git a/src/Tests/PolyfillTests_XElement.cs b/src/Tests/PolyfillTests_XElement.cs index 2db961fba..8fe6832d3 100644 --- a/src/Tests/PolyfillTests_XElement.cs +++ b/src/Tests/PolyfillTests_XElement.cs @@ -1,5 +1,18 @@ partial class PolyfillTests { + [Test] + public async Task XElement_LoadAsync_HonorsDeclaredEncoding() + { + var accentedE = (char) 0x00E9; + var xml = $"caf{accentedE}"; + // UTF-16 LE, no BOM: a UTF-8 StreamReader would mis-decode this. + var bytes = Encoding.Unicode.GetBytes(xml); + + using var stream = new MemoryStream(bytes); + var element = await XElement.LoadAsync(stream, LoadOptions.None, CancellationToken.None); + await Assert.That(element.Value).IsEqualTo($"caf{accentedE}"); + } + [Test] public async Task XElementSaveAsyncXmlWriter() { diff --git a/src/Tests/RandomNumberGeneratorPolyfillTests.cs b/src/Tests/RandomNumberGeneratorPolyfillTests.cs index b05fd0775..aca8b1599 100644 --- a/src/Tests/RandomNumberGeneratorPolyfillTests.cs +++ b/src/Tests/RandomNumberGeneratorPolyfillTests.cs @@ -2,6 +2,22 @@ public class RandomNumberGeneratorPolyfillTests { + [Test] + public async Task GetInt32_ReturnsFullRange() + { + var seen = new HashSet(); + for (var i = 0; i < 200; i++) + { + seen.Add(RandomNumberGenerator.GetInt32(0, 3)); + } + + await Assert.That(seen.Contains(0)).IsTrue(); + await Assert.That(seen.Contains(1)).IsTrue(); + // The exclusive-upper-bound-minus-one value was previously never produced. + await Assert.That(seen.Contains(2)).IsTrue(); + await Assert.That(seen.All(_ => _ is >= 0 and < 3)).IsTrue(); + } + [Test] public async Task GetInt32_Range_Valid() { diff --git a/src/Tests/TimeSpanPolyfillTests.cs b/src/Tests/TimeSpanPolyfillTests.cs index 9e554b101..e1ad14641 100644 --- a/src/Tests/TimeSpanPolyfillTests.cs +++ b/src/Tests/TimeSpanPolyfillTests.cs @@ -1,5 +1,24 @@ public class TimeSpanPolyfillTests { + [Test] + public async Task From_HappyPath_And_Overflow() + { + await Assert.That(TimeSpan.FromMicroseconds(1000000).Ticks).IsEqualTo(TimeSpan.TicksPerSecond); + await Assert.That(TimeSpan.FromDays(1).Ticks).IsEqualTo(TimeSpan.TicksPerDay); + await Assert.That(TimeSpan.FromSeconds(1, 0, 0).Ticks).IsEqualTo(TimeSpan.TicksPerSecond); + + // Components that cancel out must stay in range. Accumulation happens in a wide (decimal) + // space, so even when an individual component product overflows Int64, a cancelling + // component keeps the result in range (matching the BCL's 128-bit accumulation). + await Assert.That(TimeSpan.FromSeconds(1000000000000, -1000000000000000, 0).Ticks).IsEqualTo(0L); + await Assert.That(TimeSpan.FromSeconds(0, -9498038399810654, long.MaxValue).Ticks).IsEqualTo(-2746663629558781930L); + + // The multi-argument overloads reach the integer-based factory on every framework + // (the single-argument FromMicroseconds(double) BCL overload shadows the polyfill on net7/8). + await Assert.That(() => TimeSpan.FromDays(int.MaxValue, 0, 0, 0, 0, 0)).Throws(); + await Assert.That(() => TimeSpan.FromSeconds(long.MaxValue, 0, 0)).Throws(); + } + [Test] public async Task Microseconds_ReturnsCorrectValue() {