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());
+ }
+ }
+}