diff --git a/src/MonoMod.Backports.Shims/MonoMod.Backports.Shims.csproj b/src/MonoMod.Backports.Shims/MonoMod.Backports.Shims.csproj index d9e8431..f0333e8 100644 --- a/src/MonoMod.Backports.Shims/MonoMod.Backports.Shims.csproj +++ b/src/MonoMod.Backports.Shims/MonoMod.Backports.Shims.csproj @@ -1,4 +1,4 @@ - + true @@ -18,6 +18,10 @@ + + + + diff --git a/src/MonoMod.Backports/MonoMod.Backports.csproj b/src/MonoMod.Backports/MonoMod.Backports.csproj index 11368ee..2e3cb02 100644 --- a/src/MonoMod.Backports/MonoMod.Backports.csproj +++ b/src/MonoMod.Backports/MonoMod.Backports.csproj @@ -1,4 +1,4 @@ - + diff --git a/src/MonoMod.Backports/System/Buffers,gte_std_2.1,gte_core_2.1.cs b/src/MonoMod.Backports/System/Buffers,gte_std_2.1,gte_core_2.1.cs index 6cf0d81..162d2cb 100644 --- a/src/MonoMod.Backports/System/Buffers,gte_std_2.1,gte_core_2.1.cs +++ b/src/MonoMod.Backports/System/Buffers,gte_std_2.1,gte_core_2.1.cs @@ -24,3 +24,6 @@ [assembly: TypeForwardedTo(typeof(Utf8Parser))] [assembly: TypeForwardedTo(typeof(ArrayPool<>))] + +[assembly: TypeForwardedTo(typeof(SpanAction<,>))] +[assembly: TypeForwardedTo(typeof(ReadOnlySpanAction<,>))] diff --git a/src/MonoMod.Backports/System/Buffers,is_fx,lt_core_2.1,lt_std_2.1/SpanActions.cs b/src/MonoMod.Backports/System/Buffers,is_fx,lt_core_2.1,lt_std_2.1/SpanActions.cs new file mode 100644 index 0000000..8951448 --- /dev/null +++ b/src/MonoMod.Backports/System/Buffers,is_fx,lt_core_2.1,lt_std_2.1/SpanActions.cs @@ -0,0 +1,5 @@ +namespace System.Buffers +{ + public delegate void SpanAction(Span span, TArg arg); + public delegate void ReadOnlySpanAction(ReadOnlySpan span, TArg arg); +} \ No newline at end of file diff --git a/src/MonoMod.Backports/System/CharEx.cs b/src/MonoMod.Backports/System/CharEx.cs new file mode 100644 index 0000000..cb40e40 --- /dev/null +++ b/src/MonoMod.Backports/System/CharEx.cs @@ -0,0 +1,91 @@ +namespace System +{ + public static class CharEx + { + // there is no real reason to forward these, they are all very simple + extension(char) + { + /// Indicates whether a character is categorized as an ASCII letter. + /// The character to evaluate. + /// true if is an ASCII letter; otherwise, false. + /// + /// This determines whether the character is in the range 'A' through 'Z', inclusive, + /// or 'a' through 'z', inclusive. + /// + public static bool IsAsciiLetter(char c) => (uint)((c | 0x20) - 'a') <= 'z' - 'a'; + + /// Indicates whether a character is categorized as a lowercase ASCII letter. + /// The character to evaluate. + /// true if is a lowercase ASCII letter; otherwise, false. + /// + /// This determines whether the character is in the range 'a' through 'z', inclusive. + /// + public static bool IsAsciiLetterLower(char c) => IsBetween(c, 'a', 'z'); + + /// Indicates whether a character is categorized as an uppercase ASCII letter. + /// The character to evaluate. + /// true if is an uppercase ASCII letter; otherwise, false. + /// + /// This determines whether the character is in the range 'A' through 'Z', inclusive. + /// + public static bool IsAsciiLetterUpper(char c) => IsBetween(c, 'A', 'Z'); + + /// Indicates whether a character is categorized as an ASCII digit. + /// The character to evaluate. + /// true if is an ASCII digit; otherwise, false. + /// + /// This determines whether the character is in the range '0' through '9', inclusive. + /// + public static bool IsAsciiDigit(char c) => IsBetween(c, '0', '9'); + + /// Indicates whether a character is categorized as an ASCII letter or digit. + /// The character to evaluate. + /// true if is an ASCII letter or digit; otherwise, false. + /// + /// This determines whether the character is in the range 'A' through 'Z', inclusive, + /// 'a' through 'z', inclusive, or '0' through '9', inclusive. + /// + public static bool IsAsciiLetterOrDigit(char c) => IsAsciiLetter(c) | IsBetween(c, '0', '9'); + + /// Indicates whether a character is categorized as an ASCII hexadecimal digit. + /// The character to evaluate. + /// true if is a hexadecimal digit; otherwise, false. + /// + /// This determines whether the character is in the range '0' through '9', inclusive, + /// 'A' through 'F', inclusive, or 'a' through 'f', inclusive. + /// + public static bool IsAsciiHexDigit(char c) => IsAsciiDigit(c) || IsBetween(c, 'a', 'f') || IsBetween(c, 'A', 'F'); + + /// Indicates whether a character is categorized as an ASCII upper-case hexadecimal digit. + /// The character to evaluate. + /// true if is a hexadecimal digit; otherwise, false. + /// + /// This determines whether the character is in the range '0' through '9', inclusive, + /// or 'A' through 'F', inclusive. + /// + public static bool IsAsciiHexDigitUpper(char c) => IsAsciiDigit(c) || IsBetween(c, 'A', 'F'); + + /// Indicates whether a character is categorized as an ASCII lower-case hexadecimal digit. + /// The character to evaluate. + /// true if is a lower-case hexadecimal digit; otherwise, false. + /// + /// This determines whether the character is in the range '0' through '9', inclusive, + /// or 'a' through 'f', inclusive. + /// + public static bool IsAsciiHexDigitLower(char c) => IsAsciiDigit(c) || IsBetween(c, 'a', 'f'); + + /// Indicates whether a character is within the specified inclusive range. + /// The character to evaluate. + /// The lower bound, inclusive. + /// The upper bound, inclusive. + /// true if is within the specified range; otherwise, false. + /// + /// The method does not validate that is greater than or equal + /// to . If is less than + /// , the behavior is undefined. + /// + public static bool IsBetween(char c, char minInclusive, char maxInclusive) => + (uint)(c - minInclusive) <= (uint)(maxInclusive - minInclusive); + } + } +} \ No newline at end of file diff --git a/src/MonoMod.Backports/System/Collections/Generic/CollectionExtensionsEx.cs b/src/MonoMod.Backports/System/Collections/Generic/CollectionExtensionsEx.cs new file mode 100644 index 0000000..92e28c8 --- /dev/null +++ b/src/MonoMod.Backports/System/Collections/Generic/CollectionExtensionsEx.cs @@ -0,0 +1,100 @@ +#if NET8_0_OR_GREATER +#define HAS_LISTSPANMETHODS +#endif +#if NET7_0_OR_GREATER +#define HAS_ASREADONLY +#endif + +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +#if !HAS_LISTSPANMETHODS +using System.Runtime.InteropServices; +#endif + +namespace System.Collections.Generic +{ + [SuppressMessage("Design", "CA1002:Do not expose generic lists", + Justification = "Replicating existing APIs")] + public static class CollectionExtensionsEx + { + public static void AddRange( +#if !HAS_LISTSPANMETHODS + this +#endif + List list, params ReadOnlySpan source + ) + { +#if HAS_LISTSPANMETHODS + list.AddRange(source); +#else + ThrowHelper.ThrowIfArgumentNull(list, ExceptionArgument.list); + if (source.IsEmpty) + { + return; + } + var currentCount = list.Count; + CollectionsMarshal.SetCount(list, currentCount + source.Length); + source.CopyTo(CollectionsMarshal.AsSpan(list).Slice(currentCount + 1)); +#endif + } + + public static void InsertRange( +#if !HAS_LISTSPANMETHODS + this +#endif + List list, int index, params ReadOnlySpan source + ) + { +#if HAS_LISTSPANMETHODS + list.InsertRange(index, source); +#else + ThrowHelper.ThrowIfArgumentNull(list, ExceptionArgument.list); + if ((uint)index > (uint)list.Count) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index); + } + if (source.IsEmpty) + { + return; + } + var currentCount = list.Count; + CollectionsMarshal.SetCount(list, currentCount + source.Length); + var items = CollectionsMarshal.AsSpan(list); + if (index < currentCount) + { + items.Slice(index, currentCount - index).CopyTo(items.Slice(index + source.Length)); + } + source.CopyTo(items.Slice(index)); +#endif + } + + public static void CopyTo( +#if !HAS_LISTSPANMETHODS + this +#endif + List list, Span destination + ) + { +#if HAS_LISTSPANMETHODS + list.CopyTo(destination); +#else + ThrowHelper.ThrowIfArgumentNull(list, ExceptionArgument.list); + CollectionsMarshal.AsSpan(list).CopyTo(destination); +#endif + } + + public static ReadOnlyCollection AsReadOnly( +#if !HAS_ASREADONLY + this +#endif + IList list + ) => new(list); + + public static ReadOnlyDictionary AsReadOnly( +#if !HAS_ASREADONLY + this +#endif + IDictionary list + ) where TKey : notnull => new(list); + } +} diff --git a/src/MonoMod.Backports/System/Collections/ObjectModel/ReadOnlyDictionary,gte_fx_4.5,is_core,is_std.cs b/src/MonoMod.Backports/System/Collections/ObjectModel/ReadOnlyDictionary,gte_fx_4.5,is_core,is_std.cs new file mode 100644 index 0000000..b175366 --- /dev/null +++ b/src/MonoMod.Backports/System/Collections/ObjectModel/ReadOnlyDictionary,gte_fx_4.5,is_core,is_std.cs @@ -0,0 +1,4 @@ +using System.Collections.ObjectModel; +using System.Runtime.CompilerServices; + +[assembly: TypeForwardedTo(typeof(ReadOnlyDictionary<,>))] diff --git a/src/MonoMod.Backports/System/Collections/ObjectModel/ReadOnlyDictionary,lt_fx_4.5.cs b/src/MonoMod.Backports/System/Collections/ObjectModel/ReadOnlyDictionary,lt_fx_4.5.cs new file mode 100644 index 0000000..2eb1f90 --- /dev/null +++ b/src/MonoMod.Backports/System/Collections/ObjectModel/ReadOnlyDictionary,lt_fx_4.5.cs @@ -0,0 +1,405 @@ +using System.Collections.Generic; + +namespace System.Collections.ObjectModel +{ + public class ReadOnlyDictionary : IDictionary, IDictionary where TKey : notnull + { + private readonly IDictionary m_dictionary; // Do not rename (binary serialization) + + private KeyCollection? _keys; + private ValueCollection? _values; + + public ReadOnlyDictionary(IDictionary dictionary) + { + ArgumentNullException.ThrowIfNull(dictionary); + + m_dictionary = dictionary; + } + + /// Gets an empty . + /// An empty . + /// The returned instance is immutable and will always be empty. + public static ReadOnlyDictionary Empty { get; } = new ReadOnlyDictionary(new Dictionary()); + + protected IDictionary Dictionary => m_dictionary; + + public KeyCollection Keys => _keys ??= new KeyCollection(m_dictionary.Keys); + + public ValueCollection Values => _values ??= new ValueCollection(m_dictionary.Values); + + public bool ContainsKey(TKey key) => m_dictionary.ContainsKey(key); + + ICollection IDictionary.Keys => Keys; + + public bool TryGetValue(TKey key, out TValue value) + { + return m_dictionary.TryGetValue(key, out value!); + } + + ICollection IDictionary.Values => Values; + + public TValue this[TKey key] => m_dictionary[key]; + + void IDictionary.Add(TKey key, TValue value) + { + throw new NotSupportedException(); + } + + bool IDictionary.Remove(TKey key) + { + throw new NotSupportedException(); + } + + TValue IDictionary.this[TKey key] + { + get => m_dictionary[key]; + set => throw new NotSupportedException(); + } + + public int Count => m_dictionary.Count; + + bool ICollection>.Contains(KeyValuePair item) + { + return m_dictionary.Contains(item); + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + m_dictionary.CopyTo(array, arrayIndex); + } + + bool ICollection>.IsReadOnly => true; + + void ICollection>.Add(KeyValuePair item) + { + throw new NotSupportedException(); + } + + void ICollection>.Clear() + { + throw new NotSupportedException(); + } + + bool ICollection>.Remove(KeyValuePair item) + { + throw new NotSupportedException(); + } + + public IEnumerator> GetEnumerator() + { + return m_dictionary.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)m_dictionary).GetEnumerator(); + } + + private static bool IsCompatibleKey(object key) + { + ArgumentNullException.ThrowIfNull(key); + + return key is TKey; + } + + void IDictionary.Add(object key, object? value) + { + throw new NotSupportedException(); + } + + void IDictionary.Clear() + { + throw new NotSupportedException(); + } + + bool IDictionary.Contains(object key) + { + return IsCompatibleKey(key) && ContainsKey((TKey)key); + } + + IDictionaryEnumerator IDictionary.GetEnumerator() + { + if (m_dictionary is IDictionary d) + { + return d.GetEnumerator(); + } + return new DictionaryEnumerator(m_dictionary); + } + + bool IDictionary.IsFixedSize => true; + + bool IDictionary.IsReadOnly => true; + + ICollection IDictionary.Keys => Keys; + + void IDictionary.Remove(object key) + { + throw new NotSupportedException(); + } + + ICollection IDictionary.Values => Values; + + object? IDictionary.this[object key] + { + get + { + if (!IsCompatibleKey(key)) + { + return null; + } + + if (m_dictionary.TryGetValue((TKey)key, out TValue? value)) + { + return value; + } + else + { + return null; + } + } + set => throw new NotSupportedException(); + } + + void ICollection.CopyTo(Array array, int index) + { + CollectionHelpers.ValidateCopyToArguments(Count, array, index); + + if (array is KeyValuePair[] pairs) + { + m_dictionary.CopyTo(pairs, index); + } + else + { + if (array is DictionaryEntry[] dictEntryArray) + { + foreach (var item in m_dictionary) + { + dictEntryArray[index++] = new DictionaryEntry(item.Key, item.Value); + } + } + else + { + object[] objects = array as object[] ?? throw new ArgumentException("Incompatible array type", nameof(array)); + try + { + foreach (var item in m_dictionary) + { + objects[index++] = new KeyValuePair(item.Key, item.Value); + } + } + catch (ArrayTypeMismatchException) + { + throw new ArgumentException("Incompatible array type", nameof(array)); + } + } + } + } + + bool ICollection.IsSynchronized => false; + + object ICollection.SyncRoot => (m_dictionary is ICollection coll) ? coll.SyncRoot : this; + + private readonly struct DictionaryEnumerator : IDictionaryEnumerator + { + private readonly IDictionary _dictionary; + private readonly IEnumerator> _enumerator; + + public DictionaryEnumerator(IDictionary dictionary) + { + _dictionary = dictionary; + _enumerator = _dictionary.GetEnumerator(); + } + + public DictionaryEntry Entry + { + get => new DictionaryEntry(_enumerator.Current.Key, _enumerator.Current.Value); + } + + public object Key => _enumerator.Current.Key; + + public object? Value => _enumerator.Current.Value; + + public object Current => Entry; + + public bool MoveNext() => _enumerator.MoveNext(); + + public void Reset() => _enumerator.Reset(); + } + + public sealed class KeyCollection : ICollection, ICollection, IReadOnlyCollection + { + private readonly ICollection _collection; + + internal KeyCollection(ICollection collection) + { + ArgumentNullException.ThrowIfNull(collection); + + _collection = collection; + } + + void ICollection.Add(TKey item) + { + throw new NotSupportedException(); + } + + void ICollection.Clear() + { + throw new NotSupportedException(); + } + + public bool Contains(TKey item) + { + return _collection.Contains(item); + } + + public void CopyTo(TKey[] array, int arrayIndex) + { + _collection.CopyTo(array, arrayIndex); + } + + public int Count => _collection.Count; + + bool ICollection.IsReadOnly => true; + + bool ICollection.Remove(TKey item) + { + throw new NotSupportedException(); + } + + public IEnumerator GetEnumerator() => _collection.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_collection).GetEnumerator(); + + void ICollection.CopyTo(Array array, int index) + { + CollectionHelpers.CopyTo(_collection, array, index); + } + + bool ICollection.IsSynchronized => false; + + object ICollection.SyncRoot => (_collection is ICollection coll) ? coll.SyncRoot : this; + } + + public sealed class ValueCollection : ICollection, ICollection, IReadOnlyCollection + { + private readonly ICollection _collection; + + internal ValueCollection(ICollection collection) + { + ArgumentNullException.ThrowIfNull(collection); + + _collection = collection; + } + + void ICollection.Add(TValue item) + { + throw new NotSupportedException(); + } + + void ICollection.Clear() + { + throw new NotSupportedException(); + } + + bool ICollection.Contains(TValue item) => _collection.Contains(item); + + public void CopyTo(TValue[] array, int arrayIndex) + { + _collection.CopyTo(array, arrayIndex); + } + + public int Count => _collection.Count; + + bool ICollection.IsReadOnly => true; + + bool ICollection.Remove(TValue item) + { + throw new NotSupportedException(); + } + public IEnumerator GetEnumerator() => _collection.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_collection).GetEnumerator(); + + void ICollection.CopyTo(Array array, int index) + { + CollectionHelpers.CopyTo(_collection, array, index); + } + + bool ICollection.IsSynchronized => false; + + object ICollection.SyncRoot => (_collection is ICollection coll) ? coll.SyncRoot : this; + } + } + + internal static class CollectionHelpers + { + internal static void ValidateCopyToArguments(int sourceCount, Array array, int index) + { +#if NET + ArgumentNullException.ThrowIfNull(array); +#else + ArgumentNullException.ThrowIfNull(array); +#endif + + if (array.Rank != 1) + { + throw new ArgumentException("Multidimensional arrays are not supported", nameof(array)); + } + + if (array.GetLowerBound(0) != 0) + { + throw new ArgumentException("Non-zero lower bound", nameof(array)); + } + +#if NET + ArgumentOutOfRangeException.ThrowIfNegative(index); + ArgumentOutOfRangeException.ThrowIfGreaterThan(index, array.Length); +#else + if (index < 0 || index > array.Length) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } +#endif + + if (array.Length - index < sourceCount) + { + throw new ArgumentException("Array is too small for the index given"); + } + } + + internal static void CopyTo(ICollection collection, Array array, int index) + { + ValidateCopyToArguments(collection.Count, array, index); + + if (collection is ICollection nonGenericCollection) + { + // Easy out if the ICollection implements the non-generic ICollection + nonGenericCollection.CopyTo(array, index); + } + else if (array is T[] items) + { + collection.CopyTo(items, index); + } + else + { + // We can't cast array of value type to object[], so we don't support widening of primitive types here. + if (array is not object?[] objects) + { + throw new ArgumentException("Incompatible array type", nameof(array)); + } + + try + { + foreach (T item in collection) + { + objects[index++] = item; + } + } + catch (ArrayTypeMismatchException) + { + throw new ArgumentException("Incompatible array type", nameof(array)); + } + } + } + } +} diff --git a/src/MonoMod.Backports/System/ISpanFormattable,gte_core_6.0.cs b/src/MonoMod.Backports/System/ISpanFormattable,gte_core_6.0.cs new file mode 100644 index 0000000..9519972 --- /dev/null +++ b/src/MonoMod.Backports/System/ISpanFormattable,gte_core_6.0.cs @@ -0,0 +1,4 @@ +using System; +using System.Runtime.CompilerServices; + +[assembly: TypeForwardedTo(typeof(ISpanFormattable))] diff --git a/src/MonoMod.Backports/System/ISpanFormattable,is_fx,is_std,lt_core_6.0.cs b/src/MonoMod.Backports/System/ISpanFormattable,is_fx,is_std,lt_core_6.0.cs new file mode 100644 index 0000000..35e54b3 --- /dev/null +++ b/src/MonoMod.Backports/System/ISpanFormattable,is_fx,is_std,lt_core_6.0.cs @@ -0,0 +1,19 @@ +namespace System +{ + /// Provides functionality to format the string representation of an object into a span. + public interface ISpanFormattable : IFormattable + { + /// Tries to format the value of the current instance into the provided span of characters. + /// When this method returns, this instance's value formatted as a span of characters. + /// When this method returns, the number of characters that were written in . + /// A span containing the characters that represent a standard or custom format string that defines the acceptable format for . + /// An optional object that supplies culture-specific formatting information for . + /// if the formatting was successful; otherwise, . + /// + /// An implementation of this interface should produce the same string of characters as an implementation of + /// on the same type. + /// TryFormat should return false only if there is not enough space in the destination buffer. Any other failures should throw an exception. + /// + bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider); + } +} diff --git a/src/MonoMod.Backports/System/Index,is_fx,lt_std_2.1,lt_core_3.0.cs b/src/MonoMod.Backports/System/Index,is_fx,lt_std_2.1,lt_core_3.0.cs new file mode 100644 index 0000000..669b55f --- /dev/null +++ b/src/MonoMod.Backports/System/Index,is_fx,lt_std_2.1,lt_core_3.0.cs @@ -0,0 +1,156 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System +{ + /// Represent a type can be used to index a collection either from the start or the end. + /// + /// Index is used by the C# compiler to support the new index syntax + /// + /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; + /// int lastElement = someArray[^1]; // lastElement = 5 + /// + /// + [SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", + Justification = "Implementation taken straight from BCL")] + public readonly struct Index : IEquatable + { + private readonly int _value; + + /// Construct an Index using a value and indicating if the index is from the start or from the end. + /// The index value. it has to be zero or positive number. + /// Indicating if the index is from the start or from the end. + /// + /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Index(int value, bool fromEnd = false) + { + if (value < 0) + { + ThrowValueArgumentOutOfRange_NeedNonNegNumException(); + } + + if (fromEnd) + _value = ~value; + else + _value = value; + } + + // The following private constructors mainly created for perf reason to avoid the checks + private Index(int value) + { + _value = value; + } + + /// Create an Index pointing at first element. + public static Index Start => new Index(0); + + /// Create an Index pointing at beyond last element. + public static Index End => new Index(~0); + + /// Create an Index from the start at the position indicated by the value. + /// The index value from the start. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromStart(int value) + { + if (value < 0) + { + ThrowValueArgumentOutOfRange_NeedNonNegNumException(); + } + + return new Index(value); + } + + /// Create an Index from the end at the position indicated by the value. + /// The index value from the end. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromEnd(int value) + { + if (value < 0) + { + ThrowValueArgumentOutOfRange_NeedNonNegNumException(); + } + + return new Index(~value); + } + + /// Returns the index value. + public int Value + { + get + { + if (_value < 0) + return ~_value; + else + return _value; + } + } + + /// Indicates whether the index is from the start or the end. + public bool IsFromEnd => _value < 0; + + /// Calculate the offset from the start using the giving collection length. + /// The length of the collection that the Index will be used with. length has to be a positive value + /// + /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. + /// we don't validate either the returned offset is greater than the input length. + /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and + /// then used to index a collection will get out of range exception which will be same affect as the validation. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetOffset(int length) + { + int offset = _value; + if (IsFromEnd) + { + // offset = length - (~value) + // offset = length + (~(~value) + 1) + // offset = length + value + 1 + + offset += length + 1; + } + return offset; + } + + /// Indicates whether the current Index object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals([NotNullWhen(true)] object? value) => value is Index && _value == ((Index)value)._value; + + /// Indicates whether the current Index object is equal to another Index object. + /// An object to compare with this object + public bool Equals(Index other) => _value == other._value; + + /// Returns the hash code for this instance. + public override int GetHashCode() => _value; + + /// Converts integer number to an Index. + public static implicit operator Index(int value) => FromStart(value); + + /// Converts the value of the current Index object to its equivalent string representation. + public override string ToString() + { + if (IsFromEnd) + return ToStringFromEnd(); + + return ((uint)Value).ToString(); + } + + private static void ThrowValueArgumentOutOfRange_NeedNonNegNumException() + { + throw new ArgumentOutOfRangeException("value", "value must be non-negative"); + } + + private string ToStringFromEnd() + { +#if (!NETSTANDARD2_0 && !NETFRAMEWORK) + Span span = stackalloc char[11]; // 1 for ^ and 10 for longest possible uint value + bool formatted = ((uint)Value).TryFormat(span.Slice(1), out int charsWritten); + span[0] = '^'; + return new string(span.Slice(0, charsWritten + 1)); +#else + return '^' + Value.ToString(); +#endif + } + } +} diff --git a/src/MonoMod.Backports/System/IndexRange,gte_std_2.1,gte_core_3.0.cs b/src/MonoMod.Backports/System/IndexRange,gte_std_2.1,gte_core_3.0.cs new file mode 100644 index 0000000..14f293a --- /dev/null +++ b/src/MonoMod.Backports/System/IndexRange,gte_std_2.1,gte_core_3.0.cs @@ -0,0 +1,5 @@ +using System; +using System.Runtime.CompilerServices; + +[assembly: TypeForwardedTo(typeof(Index))] +[assembly: TypeForwardedTo(typeof(Range))] diff --git a/src/MonoMod.Backports/System/Linq/EnumerableEx.cs b/src/MonoMod.Backports/System/Linq/EnumerableEx.cs new file mode 100644 index 0000000..d902d94 --- /dev/null +++ b/src/MonoMod.Backports/System/Linq/EnumerableEx.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace System.Linq +{ + public static class EnumerableEx + { + public static IEnumerable Reverse( +#if !NET_10_OR_GREATER + this +#endif + TSource[] source) => Enumerable.Reverse(source); + } +} diff --git a/src/MonoMod.Backports/System/Range,is_fx,lt_std_2.1,lt_core_3.0.cs b/src/MonoMod.Backports/System/Range,is_fx,lt_std_2.1,lt_core_3.0.cs new file mode 100644 index 0000000..546c168 --- /dev/null +++ b/src/MonoMod.Backports/System/Range,is_fx,lt_std_2.1,lt_core_3.0.cs @@ -0,0 +1,117 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System +{ + /// Represent a range has start and end indexes. + /// + /// Range is used by the C# compiler to support the range syntax. + /// + /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 }; + /// int[] subArray1 = someArray[0..2]; // { 1, 2 } + /// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 } + /// + /// + [SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", + Justification = "Implementation taken straight from BCL")] + public readonly struct Range : IEquatable + { + /// Represent the inclusive start index of the Range. + public Index Start { get; } + + /// Represent the exclusive end index of the Range. + public Index End { get; } + + /// Construct a Range object using the start and end indexes. + /// Represent the inclusive start index of the range. + /// Represent the exclusive end index of the range. + public Range(Index start, Index end) + { + Start = start; + End = end; + } + + /// Indicates whether the current Range object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals([NotNullWhen(true)] object? value) => + value is Range r && + r.Start.Equals(Start) && + r.End.Equals(End); + + /// Indicates whether the current Range object is equal to another Range object. + /// An object to compare with this object + public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End); + + /// Returns the hash code for this instance. + public override int GetHashCode() + { + return HashCode.Combine(Start.GetHashCode(), End.GetHashCode()); + } + + /// Converts the value of the current Range object to its equivalent string representation. + public override string ToString() + { +#if (!NETSTANDARD2_0 && !NETFRAMEWORK) + Span span = stackalloc char[2 + (2 * 11)]; // 2 for "..", then for each index 1 for '^' and 10 for longest possible uint + int pos = 0; + + if (Start.IsFromEnd) + { + span[0] = '^'; + pos = 1; + } + bool formatted = ((uint)Start.Value).TryFormat(span.Slice(pos), out int charsWritten); + pos += charsWritten; + + span[pos++] = '.'; + span[pos++] = '.'; + + if (End.IsFromEnd) + { + span[pos++] = '^'; + } + formatted = ((uint)End.Value).TryFormat(span.Slice(pos), out charsWritten); + pos += charsWritten; + + return new string(span.Slice(0, pos)); +#else + return Start.ToString() + ".." + End.ToString(); +#endif + } + + /// Create a Range object starting from start index to the end of the collection. + public static Range StartAt(Index start) => new Range(start, Index.End); + + /// Create a Range object starting from first element in the collection to the end Index. + public static Range EndAt(Index end) => new Range(Index.Start, end); + + /// Create a Range object starting from first element to the end. + public static Range All => new Range(Index.Start, Index.End); + + /// Calculate the start offset and length of range object using a collection length. + /// The length of the collection that the range will be used with. length has to be a positive value. + /// + /// For performance reason, we don't validate the input length parameter against negative values. + /// It is expected Range will be used with collections which always have non negative length/count. + /// We validate the range is inside the length scope though. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public (int Offset, int Length) GetOffsetAndLength(int length) + { + int start = Start.GetOffset(length); + int end = End.GetOffset(length); + + if ((uint)end > (uint)length || (uint)start > (uint)end) + { + ThrowArgumentOutOfRangeException(); + } + + return (start, end - start); + } + + private static void ThrowArgumentOutOfRangeException() + { + throw new ArgumentOutOfRangeException("length"); + } + } +} diff --git a/src/MonoMod.Backports/System/Runtime/CompilerServices/DefaultInterpolatedStringHandler,is_std,is_fx,lt_core_6.0.cs b/src/MonoMod.Backports/System/Runtime/CompilerServices/DefaultInterpolatedStringHandler,is_std,is_fx,lt_core_6.0.cs index f039ff7..8e0a375 100644 --- a/src/MonoMod.Backports/System/Runtime/CompilerServices/DefaultInterpolatedStringHandler,is_std,is_fx,lt_core_6.0.cs +++ b/src/MonoMod.Backports/System/Runtime/CompilerServices/DefaultInterpolatedStringHandler,is_std,is_fx,lt_core_6.0.cs @@ -317,17 +317,17 @@ public void AppendFormatted(T value) string? s; if (value is IFormattable) { - // If the value can format itself directly into our buffer, do so. - /*if (value is ISpanFormattable) { - int charsWritten; - while (!((ISpanFormattable) value).TryFormat(_chars.Slice(_pos), out charsWritten, default, _provider)) // constrained call avoiding boxing for value types - { - Grow(); - } - - _pos += charsWritten; - return; - }*/ + // If the value can format itself directly into our buffer, do so. + if (value is ISpanFormattable) { + int charsWritten; + while (!((ISpanFormattable) value).TryFormat(_chars.Slice(_pos), out charsWritten, default, _provider)) // constrained call avoiding boxing for value types + { + Grow(); + } + + _pos += charsWritten; + return; + } s = ((IFormattable)value).ToString(format: null, _provider); // constrained call avoiding boxing for value types } @@ -376,7 +376,7 @@ public void AppendFormatted(T value, string? format) if (value is IFormattable) { // If the value can format itself directly into our buffer, do so. - /*if (value is ISpanFormattable) { + if (value is ISpanFormattable) { int charsWritten; while (!((ISpanFormattable) value).TryFormat(_chars.Slice(_pos), out charsWritten, format, _provider)) // constrained call avoiding boxing for value types { @@ -385,7 +385,7 @@ public void AppendFormatted(T value, string? format) _pos += charsWritten; return; - }*/ + } s = ((IFormattable)value).ToString(format, _provider); // constrained call avoiding boxing for value types } diff --git a/src/MonoMod.Backports/System/Runtime/CompilerServices/RuntimeHelpersEx.cs b/src/MonoMod.Backports/System/Runtime/CompilerServices/RuntimeHelpersEx.cs new file mode 100644 index 0000000..4fbb938 --- /dev/null +++ b/src/MonoMod.Backports/System/Runtime/CompilerServices/RuntimeHelpersEx.cs @@ -0,0 +1,113 @@ +#if NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER +#define HAS_ISREFORCONTAINSREF +#endif +#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD1_3_OR_GREATER +#define HAS_ENSUREEXECSTACK +#endif +#if NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER +#define HAS_TRYENSUREEXECSTACK +#endif + +using System.Diagnostics.CodeAnalysis; +#if !HAS_ISREFORCONTAINSREF +using System.Reflection; +#endif +using System.Runtime.Serialization; + +namespace System.Runtime.CompilerServices +{ + public static class RuntimeHelpersEx + { +#if !HAS_ISREFORCONTAINSREF + private static readonly ConditionalWeakTable> _isReferenceCache = new(); + private static readonly StrongBox _boxFalse = new(false); + private static readonly StrongBox _boxTrue = new(true); + + private static StrongBox GetIsRefOrContainsRef(Type t) + { + if (t.IsPrimitive || t.IsEnum || t.IsPointer) + { + return _boxFalse; + } + + if (!t.IsValueType) + { + return _boxTrue; + } + + foreach (var field in t.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + if (_isReferenceCache.GetValue(field.FieldType, GetIsRefOrContainsRef).Value) + { + return _boxTrue; + } + } + + return _boxFalse; + } +#endif + + extension(RuntimeHelpers) + { +#pragma warning disable SYSLIB0050 + public static object GetUninitializedObject( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + Type type) => FormatterServices.GetUninitializedObject(type); +#pragma warning restore SYSLIB0050 + + public static bool IsReferenceOrContainsReferences() => +#if HAS_ISREFORCONTAINSREF + RuntimeHelpers.IsReferenceOrContainsReferences(); +#else + _isReferenceCache.GetValue(typeof(T), GetIsRefOrContainsRef).Value; +#endif + + public static void EnsureSufficientExecutionStack() + { +#if HAS_ENSUREEXECSTACK + RuntimeHelpers.EnsureSufficientExecutionStack(); +#endif + // we do nothing + } + + public static bool TryEnsureSufficientExecutionStack() + { +#if HAS_TRYENSUREEXECSTACK + return RuntimeHelpers.TryEnsureSufficientExecutionStack(); +#elif HAS_ENSUREEXECSTACK + try + { + RuntimeHelpers.EnsureSufficientExecutionStack(); + } + catch (InsufficientExecutionStackException) + { + return false; + } + return true; +#else + // just give up + return true; +#endif + } + + public static T[] GetSubArray(T[] array, Range range) + { + ThrowHelper.ThrowIfArgumentNull(array, ExceptionArgument.array); + var (offset, length) = range.GetOffsetAndLength(array.Length); + T[] dest; + if (typeof(T[]) == array.GetType()) + { + dest = new T[length]; + } + else + { + dest = Unsafe.As(Array.CreateInstance(array.GetType().GetElementType()!, length)); + } + + Array.Copy(array, offset, dest, 0, length); + + return dest; + } + } + } +} diff --git a/src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshal,gte_core_5.0.cs b/src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshal,gte_core_5.0.cs new file mode 100644 index 0000000..2a31eff --- /dev/null +++ b/src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshal,gte_core_5.0.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: TypeForwardedTo(typeof(CollectionsMarshal))] diff --git a/src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshal,is_fx,is_std,lt_core_5.0.cs b/src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshal,is_fx,is_std,lt_core_5.0.cs new file mode 100644 index 0000000..11d840f --- /dev/null +++ b/src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshal,is_fx,is_std,lt_core_5.0.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System.Runtime.InteropServices +{ + [SuppressMessage("Design", "CA1002:Do not expose generic lists", + Justification = "Replicating existing APIs")] + public static class CollectionsMarshal + { + public static Span AsSpan(List? list) + { + if (list is null) + { + return Span.Empty; + } + + return Unsafe.As(CollectionsMarshalEx.ListFieldHolder.ItemsField.GetValue(list)); + } + } +} \ No newline at end of file diff --git a/src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshalEx.cs b/src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshalEx.cs new file mode 100644 index 0000000..a63136e --- /dev/null +++ b/src/MonoMod.Backports/System/Runtime/InteropServices/CollectionsMarshalEx.cs @@ -0,0 +1,98 @@ +#if NET5_0_OR_GREATER +#define HAS_ASSPAN +#endif +#if NET8_0_OR_GREATER +#define HAS_SETCOUNT +#endif + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +#if !HAS_SETCOUNT +using System.Reflection; +#endif + +namespace System.Runtime.InteropServices +{ + public static unsafe class CollectionsMarshalEx + { +#if !HAS_SETCOUNT + internal static class ListFieldHolder + { +#if !HAS_ASSPAN + public static FieldInfo ItemsField; +#endif + public static FieldInfo CountField; + public static FieldInfo? VersionField; + + [SuppressMessage("Performance", "CA1810:Initialize reference type static fields inline" + , Justification = "This advice is not very important here")] + static ListFieldHolder() + { + var t = typeof(List); + +#if !HAS_ASSPAN + ItemsField = t.GetField("_items", BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new NotSupportedException("Could not get List items field"); +#endif + CountField = t.GetField("_count", BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new NotSupportedException("Could not get List count field"); + VersionField = t.GetField("_version", BindingFlags.Instance | BindingFlags.NonPublic); + } + } +#endif + + extension(CollectionsMarshal) + { + [SuppressMessage("Design", "CA1002:Do not expose generic lists", + Justification = "This is replicating an existing API")] + public static void SetCount(List list, int count) + { +#if HAS_SETCOUNT + CollectionsMarshal.SetCount(list, count); +#else + ArgumentNullException.ThrowIfNull(list); + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + // setting the version field only really needs to be best effort + if (ListFieldHolder.VersionField is { } versionField) + { + versionField.SetValue(list, (int)versionField.GetValue(list)! + 1); + } + + if (count > list.Capacity) + { + // taken from List.EnsureCapacity + var newCapacity = list.Capacity == 0 ? 4 : 2 * list.Capacity; + + // Allow the list to grow to maximum possible capacity (~2G elements) before encountering overflow. + // Note that this check works even when _items.Length overflowed thanks to the (uint) cast + if ((uint)newCapacity > Array.MaxLength) + { + newCapacity = Array.MaxLength; + } + + // If the computed capacity is still less than specified, set to the original argument. + // Capacities exceeding Array.MaxLength will be surfaced as OutOfMemoryException by Array.Resize. + if (newCapacity < count) + { + newCapacity = count; + } + + list.Capacity = newCapacity; + } + + // TODO: IsReferenceOrContainsReferences + if (count < list.Count) + { + CollectionsMarshal.AsSpan(list).Slice(count + 1).Clear(); + } + + ListFieldHolder.CountField.SetValue(list, count); +#endif + } + } + } +} \ No newline at end of file diff --git a/src/MonoMod.Backports/System/Runtime/InteropServices/MarshalEx.cs b/src/MonoMod.Backports/System/Runtime/InteropServices/MarshalEx.cs index 4713c60..0ce2914 100644 --- a/src/MonoMod.Backports/System/Runtime/InteropServices/MarshalEx.cs +++ b/src/MonoMod.Backports/System/Runtime/InteropServices/MarshalEx.cs @@ -1,4 +1,5 @@ -using System.Runtime.CompilerServices; +using InlineIL; +using System.Runtime.CompilerServices; #if !NET6_0_OR_GREATER using System.Reflection; @@ -40,6 +41,24 @@ public static void SetLastPInvokeError(int error) del(error); #endif } + + public static void InitHandle(SafeHandle safeHandle, nint handle) + { + SafeHandleHelper.SetHandle(safeHandle, handle); + } + } + + private abstract class SafeHandleHelper : SafeHandle + { + private SafeHandleHelper() : base(default, default) => throw new NotSupportedException(); + + public static void SetHandle(SafeHandle safeHandle, nint handle) + { + // this method always exists and is accessible here, roslyn just wont let us call it since it is protected + IL.Push(safeHandle); + IL.Push(handle); + IL.Emit.Callvirt(MethodRef.Method(typeof(SafeHandle), "SetHandle", typeof(nint))); + } } } } diff --git a/src/MonoMod.Backports/System/Runtime/InteropServices/MemoryMarshalEx.cs b/src/MonoMod.Backports/System/Runtime/InteropServices/MemoryMarshalEx.cs new file mode 100644 index 0000000..0b48d29 --- /dev/null +++ b/src/MonoMod.Backports/System/Runtime/InteropServices/MemoryMarshalEx.cs @@ -0,0 +1,108 @@ +#if !NET6_0_OR_GREATER +using InlineIL; +#endif +using System.Runtime.CompilerServices; + +namespace System.Runtime.InteropServices +{ + public static unsafe class MemoryMarshalEx + { + extension(MemoryMarshal) + { + public static ref byte GetArrayDataReference(Array array) + { +#if NET6_0_OR_GREATER + return ref MemoryMarshal.GetArrayDataReference(array); +#else + IL.DeclareLocals(false, new LocalVar("pinned", typeof(Array)).Pinned()); + IL.Push(array); + IL.Emit.Stloc("pinned"); + return ref *(byte*)Marshal.UnsafeAddrOfPinnedArrayElement(array, 0); +#endif + } + + public static ref T GetArrayDataReference(T[] array) + { +#if NET5_0_OR_GREATER + return ref MemoryMarshal.GetArrayDataReference(array); +#else + return ref Unsafe.As(ref GetArrayDataReference((Array)array)); +#endif + } + + public static ref T AsRef(Span span) where T : struct + { +#if NETCOREAPP3_0_OR_GREATER + return ref MemoryMarshal.AsRef(span); +#else + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + ThrowHelper.ThrowArgumentException_TypeContainsReferences(typeof(T)); + } + if (span.Length < Unsafe.SizeOf()) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length); + } + return ref Unsafe.As(ref MemoryMarshal.GetReference(span)); +#endif + } + + public static ref readonly T AsRef(ReadOnlySpan span) where T : struct + { +#if NETCOREAPP3_0_OR_GREATER + return ref MemoryMarshal.AsRef(span); +#else + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + ThrowHelper.ThrowArgumentException_TypeContainsReferences(typeof(T)); + } + if (span.Length < Unsafe.SizeOf()) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length); + } + return ref Unsafe.As(ref MemoryMarshal.GetReference(span)); +#endif + } + + public static ReadOnlySpan CreateReadOnlySpanFromNullTerminated(byte* value) + { +#if NET6_0_OR_GREATER + return MemoryMarshal.CreateReadOnlySpanFromNullTerminated(value); +#else + nint current = 0; + while (value[current] != 0) + { + current += 1; + } + + if (current > int.MaxValue) + { + ThrowHelper.ThrowArgumentException("Length would exceed int.MaxValue", nameof(value)); + } + + return new ReadOnlySpan(value, (int)current); +#endif + } + + public static ReadOnlySpan CreateReadOnlySpanFromNullTerminated(char* value) + { +#if NET6_0_OR_GREATER + return MemoryMarshal.CreateReadOnlySpanFromNullTerminated(value); +#else + nint current = 0; + while (value[current] != '\0') + { + current += 1; + } + + if (current > int.MaxValue) + { + ThrowHelper.ThrowArgumentException("Length would exceed int.MaxValue", nameof(value)); + } + + return new ReadOnlySpan(value, (int)current); +#endif + } + } + } +} diff --git a/src/MonoMod.Backports/System/StringExtensions.cs b/src/MonoMod.Backports/System/StringExtensions.cs index c195e00..0c630c1 100644 --- a/src/MonoMod.Backports/System/StringExtensions.cs +++ b/src/MonoMod.Backports/System/StringExtensions.cs @@ -2,7 +2,9 @@ #define HAS_STRING_COMPARISON #endif +using System.Buffers; using System.Runtime.CompilerServices; +using System.Text; namespace System { @@ -84,5 +86,266 @@ public static int IndexOf(this string self, char value, StringComparison compari return self.IndexOf(new string(value, 1), comparison); #endif } + + public static void CopyTo(this string self, Span destination) + { + ThrowHelper.ThrowIfArgumentNull(self, ExceptionArgument.self); + self.AsSpan().CopyTo(destination); + } + + public static bool TryCopyTo(this string self, Span destination) + { + ThrowHelper.ThrowIfArgumentNull(self, ExceptionArgument.self); + return self.AsSpan().TryCopyTo(destination); + } + + private const string NewLineChars = "\n\r\f\u0085\u2028\u2029"; + + public static string ReplaceLineEndings(this string self) => self.ReplaceLineEndings(Environment.NewLine); + + public static string ReplaceLineEndings(this string self, string replacementText) + { + ThrowHelper.ThrowIfArgumentNull(self, ExceptionArgument.self); + int idxOfFirstNewlineChar = IndexOfNewlineChar(self, replacementText, out int stride); + if (idxOfFirstNewlineChar < 0) + { + return self; + } + + var firstSegment = self.AsSpan(0, idxOfFirstNewlineChar); + var remaining = self.AsSpan(idxOfFirstNewlineChar + stride); + + var builder = new StringBuilder(); + while (true) + { + var idx = IndexOfNewlineChar(remaining, replacementText, out stride); + if (idx < 0) { break; } + builder.Append(replacementText); + builder.Append(remaining.Slice(0, idx)); + remaining = remaining.Slice(idx + stride); + } + + return string.Concat(firstSegment, builder.ToString(), replacementText, remaining); + } + + private static int IndexOfNewlineChar(ReadOnlySpan text, string replacementText, out int stride) + { + stride = default; + int offset = 0; + + while (true) + { + int idx = text.IndexOfAny(NewLineChars); + + if ((uint)idx >= (uint)text.Length) + { + return -1; + } + + offset += idx; + stride = 1; // needle found + + // Did we match CR? If so, and if it's followed by LF, then we need + // to consume both chars as a single newline function match. + + if (text[idx] == '\r') + { + int nextCharIdx = idx + 1; + if ((uint)nextCharIdx < (uint)text.Length && text[nextCharIdx] == '\n') + { + stride = 2; + + if (replacementText != "\r\n") + { + return offset; + } + } + else if (replacementText != "\r") + { + return offset; + } + } + else if (replacementText.Length != 1 || replacementText[0] != text[idx]) + { + return offset; + } + + offset += stride; + text = text.Slice(idx + stride); + } + } + + extension(string) + { + /// Creates a new string by using the specified provider to control the formatting of the specified interpolated string. + /// An object that supplies culture-specific formatting information. + /// The interpolated string. + /// The string that results for formatting the interpolated string using the specified format provider. + public static string Create(IFormatProvider? provider, [InterpolatedStringHandlerArgument(nameof(provider))] ref DefaultInterpolatedStringHandler handler) => + handler.ToStringAndClear(); + + /// Creates a new string by using the specified provider to control the formatting of the specified interpolated string. + /// An object that supplies culture-specific formatting information. + /// The initial buffer that may be used as temporary space as part of the formatting operation. The contents of this buffer may be overwritten. + /// The interpolated string. + /// The string that results for formatting the interpolated string using the specified format provider. + public static string Create(IFormatProvider? provider, Span initialBuffer, [InterpolatedStringHandlerArgument(nameof(provider), nameof(initialBuffer))] ref DefaultInterpolatedStringHandler handler) => + handler.ToStringAndClear(); + + public static string Create(int length, TState state, SpanAction action) + { +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + return string.Create(length, state, action); +#else + ThrowHelper.ThrowIfArgumentNull(action, ExceptionArgument.action); + if (length <= 0) + { + if (length == 0) + { + return string.Empty; + } + + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length); + } + var str = new string('\0', length); + unsafe + { + fixed (char* p = str) + { + action(new Span(p, length), state); + } + } + return str; +#endif + } + + public static unsafe string Concat(params ReadOnlySpan values) + { +#if NET9_0_OR_GREATER + return string.Concat(values); +#else + var sum = 0; + foreach (var s in values) + { + sum += s.Length; + } + + return string.Create(sum, + (nint)(&values), + (span, state) => + { + var values = *(ReadOnlySpan*)state; + var offset = 0; + foreach (var s in values) + { + s.AsSpan().CopyTo(span.Slice(offset)); + offset += s.Length; + } + }); +#endif + } + + public static string Concat(params ReadOnlySpan args) + { +#if NET9_0_OR_GREATER + return string.Concat(args); +#else + if (args.Length <= 1) + { + return args.IsEmpty ? + string.Empty : + args[0]?.ToString() ?? string.Empty; + } + + var strings = new string[args.Length]; + + for (var i = 0; i < args.Length; i++) + { + var value = args[i]; + + strings[i] = value?.ToString() ?? string.Empty; + } + + return string.Concat(strings); +#endif + } + + public static unsafe string Concat(ReadOnlySpan str0, ReadOnlySpan str1) + { +#if NETCOREAPP3_0_OR_GREATER + return string.Concat(str0, str1); +#else + var holder = new ReadOnlySpanHolder + { + _1 = str0, + _2 = str1, + }; + return string.Create(str0.Length + str1.Length, + (nint)(&holder), + (span, state) => + { + var holder = (ReadOnlySpanHolder*)state; + holder->_1.CopyTo(span); + holder->_2.CopyTo(span.Slice(holder->_1.Length)); + }); +#endif + } + + public static unsafe string Concat(ReadOnlySpan str0, ReadOnlySpan str1, ReadOnlySpan str2) + { +#if NETCOREAPP3_0_OR_GREATER + return string.Concat(str0, str1, str2); +#else + var holder = new ReadOnlySpanHolder + { + _1 = str0, + _2 = str1, + _3 = str2, + }; + return string.Create(str0.Length + str1.Length + str2.Length, + (nint)(&holder), + (span, state) => + { + var holder = (ReadOnlySpanHolder*)state; + holder->_1.CopyTo(span); + holder->_2.CopyTo(span.Slice(holder->_1.Length)); + holder->_3.CopyTo(span.Slice(holder->_1.Length + holder->_2.Length)); + }); +#endif + } + + public static unsafe string Concat(ReadOnlySpan str0, ReadOnlySpan str1, ReadOnlySpan str2, ReadOnlySpan str3) + { +#if NETCOREAPP3_0_OR_GREATER + return string.Concat(str0, str1, str2, str3); +#else + var holder = new ReadOnlySpanHolder + { + _1 = str0, + _2 = str1, + _3 = str2, + _4 = str3, + }; + return string.Create(str0.Length + str1.Length + str2.Length + str3.Length, + (nint)(&holder), + (span, state) => + { + var holder = (ReadOnlySpanHolder*)state; + holder->_1.CopyTo(span); + holder->_2.CopyTo(span.Slice(holder->_1.Length)); + holder->_3.CopyTo(span.Slice(holder->_1.Length + holder->_2.Length)); + holder->_4.CopyTo(span.Slice(holder->_1.Length + holder->_2.Length + holder->_3.Length)); + }); +#endif + } + } + + private ref struct ReadOnlySpanHolder + { + public ReadOnlySpan _1; + public ReadOnlySpan _2; + public ReadOnlySpan _3; + public ReadOnlySpan _4; + } } } diff --git a/src/MonoMod.Backports/System/ThrowHelper.cs b/src/MonoMod.Backports/System/ThrowHelper.cs index 5d68b66..91addd0 100644 --- a/src/MonoMod.Backports/System/ThrowHelper.cs +++ b/src/MonoMod.Backports/System/ThrowHelper.cs @@ -224,6 +224,9 @@ private static Exception CreateArgumentValidationException(Array? array, int sta [DoesNotReturn, StackTraceHidden] internal static void ThrowArgumentException_TupleIncorrectType(object other) => throw new ArgumentException($"Value tuple of incorrect type (found {other.GetType()})", nameof(other)); + [DoesNotReturn, StackTraceHidden] + internal static void ThrowArgumentException_TypeContainsReferences(Type targetType) => throw new ArgumentException($"The type '{targetType}' is not supported because it contains references."); + // // ReadOnlySequence Slice validation Throws coalesced to enable inlining of the Slice // @@ -283,5 +286,8 @@ internal enum ExceptionArgument threadLocal, delay, millisecondsDelay, + list, + dictionary, + action, } } \ No newline at end of file