From 656d539c97d9076a8cf9884a2e13246658b51f4b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 00:26:28 +0000 Subject: [PATCH] fix: throw when a RingBuffer is modified during enumeration [patch] RingBuffer was the last container whose enumerator did not fail fast. On a full buffer PushBack shifts every logical index, so pushing inside a foreach silently skipped an element, and Resample or Resize repeated or dropped elements without any error. Add a version field bumped by PushBack, Clear and AllocateBuffer (which covers Resize and Resample), and check it through Enumeration.ThrowIfModified the same way ContiguousCollection does. Fixes ktsu-dev/Containers#86 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UoSUYMQMykgfsb5y6AhnKb --- Containers.Test/RingBufferTests.cs | 55 ++++++++++++++++++++++++++++++ Containers/RingBuffer.cs | 16 ++++++++- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/Containers.Test/RingBufferTests.cs b/Containers.Test/RingBufferTests.cs index 061d089..7faf41e 100644 --- a/Containers.Test/RingBufferTests.cs +++ b/Containers.Test/RingBufferTests.cs @@ -360,4 +360,59 @@ public void Constructor_MinimumLength_IsUsable() Assert.AreEqual(1, buffer.Count); Assert.AreEqual(2, buffer.At(0)); } + + [TestMethod] + public void Enumerate_PushBackDuringForeach_Throws() => + AssertModificationDuringForeachThrows(buffer => buffer.PushBack(5)); + + [TestMethod] + public void Enumerate_ClearDuringForeach_Throws() => + AssertModificationDuringForeachThrows(buffer => buffer.Clear()); + + [TestMethod] + public void Enumerate_ResizeDuringForeach_Throws() => + AssertModificationDuringForeachThrows(buffer => buffer.Resize(8)); + + [TestMethod] + public void Enumerate_ResampleDuringForeach_Throws() => + AssertModificationDuringForeachThrows(buffer => buffer.Resample(8)); + + [TestMethod] + public void Enumerate_ModifiedAfterLastElement_ThrowsOnFinalMoveNext() + { + RingBuffer buffer = new([1, 2], 2); + using IEnumerator enumerator = buffer.GetEnumerator(); + Assert.IsTrue(enumerator.MoveNext()); + Assert.IsTrue(enumerator.MoveNext()); + + buffer.PushBack(3); + + Assert.ThrowsExactly(() => enumerator.MoveNext()); + } + + [TestMethod] + public void Enumerate_ModifiedBetweenEnumerations_DoesNotThrow() + { + RingBuffer buffer = new([1, 2, 3, 4], 4); + _ = buffer.ToArray(); + buffer.PushBack(5); + + Assert.AreSequenceEqual(Enumerable.Range(2, 4), buffer); + } + + private static void AssertModificationDuringForeachThrows(Action> modify) + { + RingBuffer buffer = new([1, 2, 3, 4], 4); + + Assert.ThrowsExactly(() => + { + foreach (int item in buffer) + { + if (item == 1) + { + modify(buffer); + } + } + }); + } } diff --git a/Containers/RingBuffer.cs b/Containers/RingBuffer.cs index 635a0ea..57ed78e 100644 --- a/Containers/RingBuffer.cs +++ b/Containers/RingBuffer.cs @@ -70,6 +70,11 @@ public class RingBuffer : IReadOnlyList /// public int Count { get; private set; } // Track number of valid elements + /// + /// Incremented by every change, so an enumerator can tell the buffer changed under it. + /// + private int version; + /// /// Gets the element at the specified index in the buffer. /// @@ -141,6 +146,7 @@ private void AllocateBuffer(int length) BackIndex = 0; FrontIndex = 0; Count = 0; + version++; } /// @@ -171,6 +177,7 @@ public void PushBack(T o) // Advance back index with wraparound using bitwise AND for efficiency BackIndex = (BackIndex + 1) & (Capacity - 1); + version++; } /// @@ -285,12 +292,18 @@ public void Resample(int length) /// Returns an enumerator that iterates through the buffer. /// /// An enumerator for the buffer. - public IEnumerator GetEnumerator() + /// The buffer was modified during enumeration. + public IEnumerator GetEnumerator() => Enumerate(version); + + private IEnumerator Enumerate(int expected) { for (int i = 0; i < Count; i++) { + Enumeration.ThrowIfModified(expected, version); yield return At(i); } + + Enumeration.ThrowIfModified(expected, version); } /// @@ -320,5 +333,6 @@ public void Clear() BackIndex = 0; FrontIndex = 0; Count = 0; + version++; } }