diff --git a/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.CodedIndices.cs b/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.CodedIndices.cs index 37b5c136b..2aeee551a 100644 --- a/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.CodedIndices.cs +++ b/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.CodedIndices.cs @@ -26,16 +26,16 @@ private void AddCustomAttribute(MetadataToken ownerToken, CustomAttribute attrib table.Add(attribute, row); } - private uint AddResolutionScope(IResolutionScope? scope) + private uint AddResolutionScope(IResolutionScope? scope, bool allowDuplicates, bool preserveRid) { if (!AssertIsImported(scope)) return 0; var token = scope.MetadataToken.Table switch { - TableIndex.AssemblyRef => GetAssemblyReferenceToken(scope as AssemblyReference), - TableIndex.TypeRef => GetTypeReferenceToken(scope as TypeReference), - TableIndex.ModuleRef => GetModuleReferenceToken(scope as ModuleReference), + TableIndex.AssemblyRef => AddAssemblyReference(scope as AssemblyReference, allowDuplicates, preserveRid), + TableIndex.TypeRef => AddTypeReference(scope as TypeReference, allowDuplicates, preserveRid), + TableIndex.ModuleRef => AddModuleReference(scope as ModuleReference, allowDuplicates, preserveRid), TableIndex.Module => 0, _ => throw new ArgumentOutOfRangeException(nameof(scope)) }; diff --git a/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.TokenProvider.cs b/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.TokenProvider.cs index 1abba1f59..b3eca662b 100644 --- a/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.TokenProvider.cs +++ b/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.TokenProvider.cs @@ -10,7 +10,10 @@ public partial class DotNetDirectoryBuffer : IMetadataTokenProvider public uint GetUserStringIndex(string value) => Metadata.UserStringsStream.GetStringIndex(value); /// - public MetadataToken GetTypeReferenceToken(TypeReference? type) => AddTypeReference(type, false); + public MetadataToken GetTypeReferenceToken(TypeReference? type) + { + return AddTypeReference(type, false, false); + } /// /// Adds a type reference to the buffer. @@ -20,19 +23,25 @@ public partial class DotNetDirectoryBuffer : IMetadataTokenProvider /// true if the row is always to be added to the end of the buffer, false if a duplicated row /// is supposed to be removed and the token of the original should be returned instead. /// + /// + /// true if the metadata token of the type should be preserved, false otherwise. + /// /// The newly assigned metadata token. - public MetadataToken AddTypeReference(TypeReference? type, bool allowDuplicates) + public MetadataToken AddTypeReference(TypeReference? type, bool allowDuplicates, bool preserveRid) { if (!AssertIsImported(type)) return MetadataToken.Zero; var table = Metadata.TablesStream.GetDistinctTable(TableIndex.TypeRef); var row = new TypeReferenceRow( - AddResolutionScope(type.Scope), + AddResolutionScope(type.Scope, allowDuplicates, preserveRid), Metadata.StringsStream.GetStringIndex(type.Name), Metadata.StringsStream.GetStringIndex(type.Namespace)); - var token = table.Add(row, allowDuplicates); + var token = preserveRid + ? table.Insert(type.MetadataToken.Rid, row, allowDuplicates) + : table.Add(row, allowDuplicates); + _tokenMapping.Register(type, token); AddCustomAttributes(token, type); return token; @@ -164,7 +173,7 @@ public MetadataToken AddStandAloneSignature(StandAloneSignature? signature, bool /// public MetadataToken GetAssemblyReferenceToken(AssemblyReference? assembly) { - return AddAssemblyReference(assembly, false); + return AddAssemblyReference(assembly, false, false); } /// @@ -175,8 +184,11 @@ public MetadataToken GetAssemblyReferenceToken(AssemblyReference? assembly) /// true if the row is always to be added to the end of the buffer, false if a duplicated row /// is supposed to be removed and the token of the original should be returned instead. /// + /// + /// true if the metadata token of the assembly should be preserved, false otherwise. + /// /// The newly assigned metadata token. - public MetadataToken AddAssemblyReference(AssemblyReference? assembly, bool allowDuplicates) + public MetadataToken AddAssemblyReference(AssemblyReference? assembly, bool allowDuplicates, bool preserveRid) { if (assembly is null || !AssertIsImported(assembly)) return MetadataToken.Zero; @@ -193,7 +205,10 @@ public MetadataToken AddAssemblyReference(AssemblyReference? assembly, bool allo Metadata.StringsStream.GetStringIndex(assembly.Culture), Metadata.BlobStream.GetBlobIndex(assembly.HashValue)); - var token = table.Add(row, allowDuplicates); + var token = preserveRid + ? table.Insert(assembly.MetadataToken.Rid, row, allowDuplicates) + : table.Add(row, allowDuplicates); + AddCustomAttributes(token, assembly); return token; } @@ -205,7 +220,7 @@ public MetadataToken AddAssemblyReference(AssemblyReference? assembly, bool allo /// The new metadata token assigned to the module reference. public MetadataToken GetModuleReferenceToken(ModuleReference? reference) { - return AddModuleReference(reference, false); + return AddModuleReference(reference, false, false); } /// @@ -216,8 +231,11 @@ public MetadataToken GetModuleReferenceToken(ModuleReference? reference) /// true if the row is always to be added to the end of the buffer, false if a duplicated row /// is supposed to be removed and the token of the original should be returned instead. /// + /// + /// true if the metadata token of the module should be preserved, false otherwise. + /// /// The newly assigned metadata token. - public MetadataToken AddModuleReference(ModuleReference? reference, bool allowDuplicates) + public MetadataToken AddModuleReference(ModuleReference? reference, bool allowDuplicates, bool preserveRid) { if (!AssertIsImported(reference)) return MetadataToken.Zero; @@ -225,7 +243,10 @@ public MetadataToken AddModuleReference(ModuleReference? reference, bool allowDu var table = Metadata.TablesStream.GetDistinctTable(TableIndex.ModuleRef); var row = new ModuleReferenceRow(Metadata.StringsStream.GetStringIndex(reference.Name)); - var token = table.Add(row, allowDuplicates); + var token = preserveRid + ? table.Insert(reference.MetadataToken.Rid, row, allowDuplicates) + : table.Add(row, allowDuplicates); + AddCustomAttributes(token, reference); return token; } diff --git a/src/AsmResolver.DotNet/Builder/DotNetDirectoryFactory.cs b/src/AsmResolver.DotNet/Builder/DotNetDirectoryFactory.cs index 705fda930..7c0619f9f 100644 --- a/src/AsmResolver.DotNet/Builder/DotNetDirectoryFactory.cs +++ b/src/AsmResolver.DotNet/Builder/DotNetDirectoryFactory.cs @@ -212,19 +212,19 @@ private void ImportBasicTablesIfSpecified(ModuleDefinition module, DotNetDirecto if ((MetadataBuilderFlags & MetadataBuilderFlags.PreserveAssemblyReferenceIndices) != 0) { ImportTables(module, TableIndex.AssemblyRef, - r => buffer.AddAssemblyReference(r, true)); + r => buffer.AddAssemblyReference(r, true, true)); } if ((MetadataBuilderFlags & MetadataBuilderFlags.PreserveModuleReferenceIndices) != 0) { ImportTables(module, TableIndex.ModuleRef, - r => buffer.AddModuleReference(r, true)); + r => buffer.AddModuleReference(r, true, true)); } if ((MetadataBuilderFlags & MetadataBuilderFlags.PreserveTypeReferenceIndices) != 0) { ImportTables(module, TableIndex.TypeRef, - r => buffer.AddTypeReference(r, true)); + r => buffer.AddTypeReference(r, true, true)); } } diff --git a/src/AsmResolver.DotNet/Builder/Metadata/Tables/DistinctMetadataTableBuffer.cs b/src/AsmResolver.DotNet/Builder/Metadata/Tables/DistinctMetadataTableBuffer.cs index 2fba58233..32a440059 100644 --- a/src/AsmResolver.DotNet/Builder/Metadata/Tables/DistinctMetadataTableBuffer.cs +++ b/src/AsmResolver.DotNet/Builder/Metadata/Tables/DistinctMetadataTableBuffer.cs @@ -50,6 +50,33 @@ public TRow this[uint rid] /// public MetadataToken Add(in TRow row) => Add(row, false); + /// + public MetadataToken Insert(uint rid, in TRow row) => Insert(rid, row, false); + + /// + /// Inserts a row into the metadata table at the provided row identifier. + /// + /// The row identifier. + /// The row to add. + /// + /// true if the row is always to be added to the end of the buffer, false if a duplicated row + /// is supposed to be removed and the token of the original should be returned instead. + /// The metadata token that this row was assigned to. + public MetadataToken Insert(uint rid, in TRow row, bool allowDuplicates) + { + if (!_entries.TryGetValue(row, out var token)) + { + token = _underlyingBuffer.Insert(rid, in row); + _entries.Add(row, token); + } + else if (allowDuplicates) + { + token = _underlyingBuffer.Insert(rid, in row); + } + + return token; + } + /// /// Adds a row to the metadata table buffer. /// diff --git a/src/AsmResolver.DotNet/Builder/Metadata/Tables/IMetadataTableBuffer.cs b/src/AsmResolver.DotNet/Builder/Metadata/Tables/IMetadataTableBuffer.cs index bcf1b8bfd..5cecd39db 100644 --- a/src/AsmResolver.DotNet/Builder/Metadata/Tables/IMetadataTableBuffer.cs +++ b/src/AsmResolver.DotNet/Builder/Metadata/Tables/IMetadataTableBuffer.cs @@ -56,5 +56,13 @@ TRow this[uint rid] /// The row to add. /// The metadata token that this row was assigned to. MetadataToken Add(in TRow row); + + /// + /// Inserts a row into the metadata table at the provided row identifier. + /// + /// The row identifier. + /// The row to add. + /// The metadata token that this row was assigned to. + MetadataToken Insert(uint rid, in TRow row); } } diff --git a/src/AsmResolver.DotNet/Builder/Metadata/Tables/UnsortedMetadataTableBuffer.cs b/src/AsmResolver.DotNet/Builder/Metadata/Tables/UnsortedMetadataTableBuffer.cs index e94a81d44..fe754d7ac 100644 --- a/src/AsmResolver.DotNet/Builder/Metadata/Tables/UnsortedMetadataTableBuffer.cs +++ b/src/AsmResolver.DotNet/Builder/Metadata/Tables/UnsortedMetadataTableBuffer.cs @@ -15,7 +15,9 @@ public class UnsortedMetadataTableBuffer : IMetadataTableBuffer where TRow : struct, IMetadataRow { private readonly RefList _entries = new(); + private readonly BitList _available = new(); private readonly MetadataTable _table; + private uint _currentRid; /// /// Creates a new unsorted metadata table buffer. @@ -33,7 +35,11 @@ public UnsortedMetadataTableBuffer(MetadataTable table) public virtual TRow this[uint rid] { get => _entries[(int) (rid - 1)]; - set => _entries[(int) (rid - 1)] = value; + set + { + _entries[(int) (rid - 1)] = value; + _available[(int) (rid - 1)] = false; + } } /// @@ -42,8 +48,45 @@ public virtual TRow this[uint rid] /// public virtual MetadataToken Add(in TRow row) { - _entries.Add(row); - return new MetadataToken(_table.TableIndex, (uint) _entries.Count); + // Move over unavailable slots. + while (_currentRid < _available.Count && !_available[(int) _currentRid]) + _currentRid++; + + // If we moved over all entries, we're adding to the end. + if (_currentRid == _entries.Count) + { + _currentRid++; + } + + return Insert(_currentRid++, row); + } + + /// + public MetadataToken Insert(uint rid, in TRow row) + { + EnsureRowsAllocated(rid); + + var token = new MetadataToken(_table.TableIndex, rid); + + if (!_available[(int) (rid - 1)]) + { + if (EqualityComparer.Default.Equals(row, _entries[(int) (rid - 1)])) + return token; + + throw new InvalidOperationException($"Token 0x{token.ToString()} is already in use."); + } + + this[rid] = row; + return token; + } + + private void EnsureRowsAllocated(uint rid) + { + while (_entries.Count < rid) + { + _entries.Add(default); + _available.Add(true); + } } /// diff --git a/src/AsmResolver.DotNet/Builder/PEImageBuildResult.cs b/src/AsmResolver.DotNet/Builder/PEImageBuildResult.cs index faec68be5..95aef83eb 100644 --- a/src/AsmResolver.DotNet/Builder/PEImageBuildResult.cs +++ b/src/AsmResolver.DotNet/Builder/PEImageBuildResult.cs @@ -34,7 +34,7 @@ public IPEImage? ConstructedImage /// Gets a value indicating whether the image was constructed successfully or not. /// [MemberNotNullWhen(false, nameof(ConstructedImage))] - public bool HasFailed => ConstructedImage is null; + public bool HasFailed => DiagnosticBag.IsFatal; /// /// Gets the bag containing the diagnostics that were collected during the construction of the image. diff --git a/src/AsmResolver/Collections/BitList.cs b/src/AsmResolver/Collections/BitList.cs new file mode 100644 index 000000000..c4224cd04 --- /dev/null +++ b/src/AsmResolver/Collections/BitList.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace AsmResolver.Collections +{ + /// + /// Represents a bit vector that can be resized dynamically. + /// + public class BitList : IList + { + private const int WordSize = sizeof(int) * 8; + private uint[] _words; + private int _version; + + /// + /// Creates a new bit list. + /// + public BitList() + { + _words = new uint[1]; + } + + /// + /// Creates a new bit list. + /// + /// The initial number of bits that the buffer should at least be able to store. + public BitList(int capacity) + { + _words = new uint[((uint) capacity).Align(WordSize)]; + } + + /// + public int Count + { + get; + private set; + } + + /// + public bool IsReadOnly => false; + + /// + public bool this[int index] + { + get + { + if (index >= Count) + throw new IndexOutOfRangeException(); + + (int wordIndex, int bitIndex) = SplitWordBitIndex(index); + return (_words[wordIndex] >> bitIndex & 1) != 0; + } + set + { + if (index >= Count) + throw new IndexOutOfRangeException(); + + (int wordIndex, int bitIndex) = SplitWordBitIndex(index); + _words[wordIndex] = (_words[wordIndex] & ~(1u << bitIndex)) | (value ? 1u << bitIndex : 0u); + _version++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static (int wordIndex, int bitIndex) SplitWordBitIndex(int index) + { + int wordIndex = Math.DivRem(index, WordSize, out int offset); + return (wordIndex, offset); + } + + /// + public void Add(bool item) + { + EnsureCapacity(Count + 1); + Count++; + this[Count - 1] = item; + _version++; + } + + /// + public void Clear() => Count = 0; + + /// + public bool Contains(bool item) => IndexOf(item) != -1; + + /// + public void CopyTo(bool[] array, int arrayIndex) + { + for (int i = 0; i < Count; i++) + array[arrayIndex + i] = this[i]; + } + + /// + public bool Remove(bool item) + { + int index = IndexOf(item); + if (index == -1) + return false; + + RemoveAt(index); + return true; + } + + /// + public int IndexOf(bool item) + { + for (int i = 0; i < Count; i++) + { + (int wordIndex, int bitIndex) = SplitWordBitIndex(i); + if ((_words[wordIndex] >> bitIndex & 1) != 0 == item) + return i; + } + + return -1; + } + + /// + public void Insert(int index, bool item) + { + if (index > Count) + throw new IndexOutOfRangeException(); + + EnsureCapacity(Count++); + (int wordIndex, int bitIndex) = SplitWordBitIndex(index); + + uint carry = _words[wordIndex] & (1u << (WordSize - 1)); + + // Insert bit into current word. + uint lowerMask = (1u << bitIndex) - 1; + uint upperMask = ~lowerMask; + _words[wordIndex] = (_words[wordIndex] & upperMask) << 1 // Shift left-side of the bit index by one + | (item ? 1u << bitIndex : 0u) // Insert bit. + | (_words[wordIndex] & lowerMask); // Keep right-side of the bit. + + for (int i = wordIndex + 1; i < _words.Length; i++) + { + uint nextCarry = _words[i] & (1u << (WordSize - 1)); + _words[i] = (_words[i] << 1) | (carry >> (WordSize - 1)); + carry = nextCarry; + } + + _version++; + } + + /// + public void RemoveAt(int index) + { + Count--; + (int wordIndex, int bitIndex) = SplitWordBitIndex(index); + + // Note we check both word count and actual bit count. Words in the buffer might contain garbage data for + // every bit index i >= Count. Also, there might be exactly enough words allocated for Count bits, i.e. + // there might not be a "next" word. + uint borrow = wordIndex + 1 < _words.Length && ((uint) index).Align(WordSize) < Count + ? _words[wordIndex + 1] & 1 + : 0; + + uint lowerMask = (1u << bitIndex) - 1; + uint upperMask = ~((1u << (bitIndex + 1)) - 1); + _words[wordIndex] = (_words[wordIndex] & upperMask) >> 1 // Shift left-side of the bit index by one + | (_words[wordIndex] & lowerMask) // Keep right-side of the bit. + | borrow << (WordSize - 1); // Copy first bit of next word into last bit of current. + + for (int i = wordIndex + 1; i < _words.Length; i++) + { + uint nextBorrow = i + 1 < _words.Length && ((uint) index).Align(WordSize) < Count + ? _words[i + 1] & 1 + : 0; + + _words[i] = (_words[i] >> 1) | (borrow << (WordSize - 1)); + borrow = nextBorrow; + } + + _version++; + } + + /// + /// Ensures the provided number of bits can be stored in the bit list. + /// + /// The number of bits to store in the list. + public void EnsureCapacity(int capacity) + { + if (capacity < WordSize * _words.Length) + return; + + int newWordCount = (int) (((uint) capacity).Align(WordSize) / 8); + Array.Resize(ref _words, newWordCount); + } + + /// + /// Returns an enumerator for all bits in the bit vector. + /// + /// The enumerator. + public Enumerator GetEnumerator() => new(this); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Represents an enumerator that iterates over all bits in a bit list. + /// + public struct Enumerator : IEnumerator + { + private readonly BitList _list; + private readonly int _version; + private int _index = -1; + + /// + /// Creates a new bit enumerator. + /// + /// The list to enumerate. + public Enumerator(BitList list) + { + _version = list._version; + _list = list; + } + + /// + public bool MoveNext() + { + if (_version != _list._version) + throw new InvalidOperationException("Collection was modified."); + + if (_index >= _list.Count) + return false; + + _index++; + return true; + } + + /// + public void Reset() => _index = -1; + + /// + public bool Current => _list[_index]; + + /// + object IEnumerator.Current => Current; + + /// + public void Dispose() + { + } + } + } +} diff --git a/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/TypeRefTokenPreservationTest.cs b/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/TypeRefTokenPreservationTest.cs index ebb5bc237..fa233b1e8 100644 --- a/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/TypeRefTokenPreservationTest.cs +++ b/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/TypeRefTokenPreservationTest.cs @@ -99,5 +99,19 @@ public void PreserveDuplicatedTypeRefs() references.Select(r => r.MetadataToken).ToHashSet(), newObjectReferences.Select(r => r.MetadataToken).ToHashSet()); } + + [Fact] + public void PreserveNestedTypeRefOrdering() + { + // https://github.com/Washi1337/AsmResolver/issues/329 + + var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld_UnusualNestedTypeRefOrder); + var originalTypeRefs = GetMembers(module, TableIndex.TypeRef); + + var newModule = RebuildAndReloadModule(module, MetadataBuilderFlags.PreserveTypeReferenceIndices); + var newTypeRefs = GetMembers(newModule, TableIndex.TypeRef); + + Assert.Equal(originalTypeRefs, newTypeRefs.Take(originalTypeRefs.Count), Comparer); + } } } diff --git a/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs b/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs index c2e4010ea..413f1fa55 100644 --- a/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs +++ b/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs @@ -164,6 +164,13 @@ public static byte[] HelloWorld_SingleFile_V6_WithResources { } } + public static byte[] HelloWorld_UnusualNestedTypeRefOrder { + get { + object obj = ResourceManager.GetObject("HelloWorld_UnusualNestedTypeRefOrder", resourceCulture); + return ((byte[])(obj)); + } + } + public static byte[] Assembly1_Forwarder { get { object obj = ResourceManager.GetObject("Assembly1_Forwarder", resourceCulture); diff --git a/test/AsmResolver.DotNet.Tests/Properties/Resources.resx b/test/AsmResolver.DotNet.Tests/Properties/Resources.resx index 5e722a4a9..ecc3273dd 100644 --- a/test/AsmResolver.DotNet.Tests/Properties/Resources.resx +++ b/test/AsmResolver.DotNet.Tests/Properties/Resources.resx @@ -69,6 +69,9 @@ ..\Resources\HelloWorld.SingleFile.v6.WithResources.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\Resources\HelloWorld.UnusualNestedTypeRefOrder.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + ..\Resources\Assembly1_Forwarder.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 diff --git a/test/AsmResolver.DotNet.Tests/Resources/HelloWorld.UnusualNestedTypeRefOrder.exe b/test/AsmResolver.DotNet.Tests/Resources/HelloWorld.UnusualNestedTypeRefOrder.exe new file mode 100644 index 000000000..c70040376 Binary files /dev/null and b/test/AsmResolver.DotNet.Tests/Resources/HelloWorld.UnusualNestedTypeRefOrder.exe differ diff --git a/test/AsmResolver.Tests/Collections/BitListTest.cs b/test/AsmResolver.Tests/Collections/BitListTest.cs new file mode 100644 index 000000000..402f8ec2e --- /dev/null +++ b/test/AsmResolver.Tests/Collections/BitListTest.cs @@ -0,0 +1,111 @@ +using System.Linq; +using AsmResolver.Collections; +using Xunit; + +namespace AsmResolver.Tests.Collections +{ + public class BitListTest + { + [Fact] + public void Add() + { + var list = new BitList + { + true, + false, + true, + true, + false, + }; + + Assert.Equal(new[] + { + true, + false, + true, + true, + false + }, list.ToArray()); + } + + [Fact] + public void Insert() + { + var list = new BitList + { + true, + false, + true, + true, + false, + }; + + list.Insert(1, true); + + Assert.Equal(new[] + { + true, + true, + false, + true, + true, + false + }, list.ToArray()); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void InsertIntoLarge(bool parity) + { + var list = new BitList(); + for (int i = 0; i < 100; i++) + list.Add(i % 2 == 0 == parity); + + list.Insert(0, !parity); + + Assert.Equal(101, list.Count); + bool[] expected = Enumerable.Range(0, 101).Select(i => i % 2 == 1 == parity).ToArray(); + Assert.Equal(expected, list.ToArray()); + } + + [Fact] + public void RemoveAt() + { + var list = new BitList + { + true, + false, + true, + true, + false, + }; + + list.RemoveAt(3); + + Assert.Equal(new[] + { + true, + false, + true, + false, + }, list.ToArray()); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void RemoveAtLarge(bool parity) + { + var list = new BitList(); + for (int i = 0; i < 100; i++) + list.Add(i % 2 == 0 == parity); + + list.RemoveAt(0); + + Assert.Equal(99, list.Count); + bool[] expected = Enumerable.Range(0, 99).Select(i => i % 2 == 1 == parity).ToArray(); + Assert.Equal(expected, list.ToArray()); + } + } +}