From 69e4d0b84fcb7d509f6006c498a4530d33236b24 Mon Sep 17 00:00:00 2001 From: bitc0der <59016822+bitc0der@users.noreply.github.com> Date: Sun, 30 Nov 2025 19:48:08 +0700 Subject: [PATCH 1/4] Improve hash --- .../Patch/RollingHashTests.cs | 49 +++++++++++++++++++ src/BitSoft.BinaryTools/Patch/RollingHash.cs | 15 +++--- 2 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 src/BitSoft.BinaryTools.Tests/Patch/RollingHashTests.cs diff --git a/src/BitSoft.BinaryTools.Tests/Patch/RollingHashTests.cs b/src/BitSoft.BinaryTools.Tests/Patch/RollingHashTests.cs new file mode 100644 index 0000000..e2e5608 --- /dev/null +++ b/src/BitSoft.BinaryTools.Tests/Patch/RollingHashTests.cs @@ -0,0 +1,49 @@ +using BitSoft.BinaryTools.Patch; + +namespace BitSoft.BinaryTools.Tests.Patch; + +using System; + +[TestFixture] +public class RollingHashTests +{ + [TestCase(1024, 32)] + [TestCase(1024, 64)] + [TestCase(1024, 128)] + [TestCase(1024, 256)] + public void Should_CalculateHash(int bufferLength, int bufferSize) + { + // Arrange + var buffer = new byte[bufferLength]; + + Random.Shared.NextBytes(buffer); + + // Act & Assert + var initialSpan = buffer.AsSpan(start: 0, length: bufferSize); + var rollingHash = RollingHash.Create(initialSpan); + var gRollingHash = new Adler32RollingHash(bufferSize); + gRollingHash.CalculateInitialHash(initialSpan); + + for (var i = 0; i < bufferLength - bufferSize; i++) + { + var span = buffer.AsSpan(start: i, length: bufferSize); + + var spanHash = RollingHash.Create(span); + + Assert.That( + actual: rollingHash.GetChecksum(), + expression: Is.EqualTo(spanHash.GetChecksum()), + message: $"Failed as position '{i}'" + ); + + if (i < bufferLength - bufferSize - 1) + { + var oldByte = buffer[i]; + var newByte = buffer[i + bufferSize]; + + rollingHash.Update(removed: oldByte, added: newByte); + gRollingHash.Roll(oldByte, newByte); + } + } + } +} diff --git a/src/BitSoft.BinaryTools/Patch/RollingHash.cs b/src/BitSoft.BinaryTools/Patch/RollingHash.cs index 4363f11..6337fd7 100644 --- a/src/BitSoft.BinaryTools/Patch/RollingHash.cs +++ b/src/BitSoft.BinaryTools/Patch/RollingHash.cs @@ -8,9 +8,9 @@ public struct RollingHash private uint _a; private uint _b; - private readonly uint _length; + private readonly long _length; - private RollingHash(uint a, uint b, uint length) + private RollingHash(uint a, uint b, int length) { _a = a; _b = b; @@ -22,21 +22,22 @@ public static RollingHash Create(ReadOnlySpan data) uint a = 1; uint b = 0; - for (var i = 0; i < data.Length; i++) + foreach (var value in data) { - var value = data[i]; - a = (a + value) % Base; b = (b + a) % Base; } - return new RollingHash(a: a, b: b, length: (uint)data.Length); + return new RollingHash(a: a, b: b, length: data.Length); } public void Update(byte removed, byte added) { _a = (_a - removed + added) % Base; - _b = (_b - _length * removed + _a - 1) % Base; + + var temp = _b - _length * removed + _a - 1; + + _b = (uint)((temp % Base + Base) % Base); } public uint GetChecksum() => (_b << 16) | _a; From 4c05d828bfdb266286e0394fd455560ed549381d Mon Sep 17 00:00:00 2001 From: bitc0der <59016822+bitc0der@users.noreply.github.com> Date: Sun, 30 Nov 2025 19:49:32 +0700 Subject: [PATCH 2/4] Adjust test params --- src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs b/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs index 8ab4ceb..2b5895d 100644 --- a/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs +++ b/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs @@ -124,11 +124,9 @@ await BinaryPatch.CreateAsync( } [Ignore("Performance test")] - [TestCase(1024 * 1024, 512, 0, 128)] - [TestCase(1024 * 1024, 512, 1, 128)] - [TestCase(1024 * 1024, 512, 5, 128)] - [TestCase(1024 * 1024, 1024, 5, 128)] - [TestCase(1024 * 1024, 4096, 5, 128)] + [TestCase(1024 * 1024, 256, 0, 128)] + [TestCase(1024 * 1024, 256, 1, 128)] + [TestCase(1024 * 1024, 256, 5, 128)] public async Task Should_CreatePatch(int bufferLength, int blockSize, int changedBlocks, int changeSize) { // Arrange From e4bfe118348977aa35b4ee00a244fc1799e410b1 Mon Sep 17 00:00:00 2001 From: bitc0der <59016822+bitc0der@users.noreply.github.com> Date: Sun, 30 Nov 2025 19:59:11 +0700 Subject: [PATCH 3/4] Minor cleanup --- src/BitSoft.BinaryTools.Tests/Patch/RollingHashTests.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/BitSoft.BinaryTools.Tests/Patch/RollingHashTests.cs b/src/BitSoft.BinaryTools.Tests/Patch/RollingHashTests.cs index e2e5608..179ab0b 100644 --- a/src/BitSoft.BinaryTools.Tests/Patch/RollingHashTests.cs +++ b/src/BitSoft.BinaryTools.Tests/Patch/RollingHashTests.cs @@ -5,7 +5,7 @@ namespace BitSoft.BinaryTools.Tests.Patch; using System; [TestFixture] -public class RollingHashTests +public sealed class RollingHashTests { [TestCase(1024, 32)] [TestCase(1024, 64)] @@ -21,8 +21,6 @@ public void Should_CalculateHash(int bufferLength, int bufferSize) // Act & Assert var initialSpan = buffer.AsSpan(start: 0, length: bufferSize); var rollingHash = RollingHash.Create(initialSpan); - var gRollingHash = new Adler32RollingHash(bufferSize); - gRollingHash.CalculateInitialHash(initialSpan); for (var i = 0; i < bufferLength - bufferSize; i++) { @@ -42,7 +40,6 @@ public void Should_CalculateHash(int bufferLength, int bufferSize) var newByte = buffer[i + bufferSize]; rollingHash.Update(removed: oldByte, added: newByte); - gRollingHash.Roll(oldByte, newByte); } } } From f9dea40b9b6bef19e7fb97c043dbeca7bad6e426 Mon Sep 17 00:00:00 2001 From: bitc0der <59016822+bitc0der@users.noreply.github.com> Date: Sun, 18 Jan 2026 13:22:14 +0700 Subject: [PATCH 4/4] Hash --- .../Patch/Adler32RollingHash.cs | 56 +++++++++++++++++++ .../Patch/Adler32RollingHashTests.cs | 31 ++++++++++ .../Patch/RollingHashTests.cs | 3 + src/BitSoft.BinaryTools/Patch/RollingHash.cs | 23 +++++--- 4 files changed, 104 insertions(+), 9 deletions(-) create mode 100644 src/BitSoft.BinaryTools.Tests/Patch/Adler32RollingHash.cs create mode 100644 src/BitSoft.BinaryTools.Tests/Patch/Adler32RollingHashTests.cs diff --git a/src/BitSoft.BinaryTools.Tests/Patch/Adler32RollingHash.cs b/src/BitSoft.BinaryTools.Tests/Patch/Adler32RollingHash.cs new file mode 100644 index 0000000..9544894 --- /dev/null +++ b/src/BitSoft.BinaryTools.Tests/Patch/Adler32RollingHash.cs @@ -0,0 +1,56 @@ +using System; + +public class Adler32RollingHash +{ + private const uint ModAdler = 65521; + private uint _s1 = 1; + private uint _s2 = 0; + private readonly int _windowSize; + + public Adler32RollingHash(int windowSize) + { + if (windowSize <= 0) + throw new ArgumentException("Window size must be positive.", nameof(windowSize)); + _windowSize = windowSize; + } + + public void CalculateInitialHash(ReadOnlySpan data) + { + if (data.Length != _windowSize) + throw new ArgumentException("Initial data length must match window size."); + + _s1 = 1; _s2 = 0; + int len = data.Length; int i = 0; + while (len > 0) + { + int k = Math.Min(len, 3800); len -= k; + while (k-- > 0) { _s1 += data[i++]; _s2 += _s1; } + _s1 %= ModAdler; _s2 %= ModAdler; + } + } + + public void Roll(byte byteOut, byte byteIn) + { + var s1_new = (int)_s1 - byteOut + byteIn; + _s1 = (uint)((s1_new % ModAdler + ModAdler) % ModAdler); + + long diff = (long)_windowSize * byteOut; + long tempS2 = (long)_s2 - diff + (long)_s1 - 1; + _s2 = (uint)((tempS2 % ModAdler + ModAdler) % ModAdler); + } + + public uint Checksum => (_s2 << 16) | _s1; + + public static uint CalculateFullChecksum(ReadOnlySpan data) + { + // Simple, non-optimized full checksum for verification + uint s1 = 1; + uint s2 = 0; + foreach (byte b in data) + { + s1 = (s1 + b) % ModAdler; + s2 = (s2 + s1) % ModAdler; + } + return (s2 << 16) | s1; + } +} diff --git a/src/BitSoft.BinaryTools.Tests/Patch/Adler32RollingHashTests.cs b/src/BitSoft.BinaryTools.Tests/Patch/Adler32RollingHashTests.cs new file mode 100644 index 0000000..cd6436c --- /dev/null +++ b/src/BitSoft.BinaryTools.Tests/Patch/Adler32RollingHashTests.cs @@ -0,0 +1,31 @@ +using System; + +public class Adler32RollingHashTests_Scalability +{ + [Test] + public void TestRollingHashWithMegaByteDataBuffer() + { + int Megabyte = 1024 * 1024; + int bufferSize = 1 * Megabyte; + int windowSize = 4096; + + var data = new byte[bufferSize]; + Random.Shared.NextBytes(data); + + Adler32RollingHash rollingHash = new (windowSize); + rollingHash.CalculateInitialHash(data.AsSpan(start: 0, length: windowSize)); + + for (int i = 0; i < data.Length - windowSize; i++) + { + byte byteOut = data[i]; + byte byteIn = data[i + windowSize]; + + rollingHash.Roll(byteOut, byteIn); + + var span = data.AsSpan(start: i + 1, length: windowSize); + + var expectedChecksum = Adler32RollingHash.CalculateFullChecksum(span); + Assert.That(expectedChecksum, Is.EqualTo(rollingHash.Checksum)); + } + } +} diff --git a/src/BitSoft.BinaryTools.Tests/Patch/RollingHashTests.cs b/src/BitSoft.BinaryTools.Tests/Patch/RollingHashTests.cs index 179ab0b..385dcda 100644 --- a/src/BitSoft.BinaryTools.Tests/Patch/RollingHashTests.cs +++ b/src/BitSoft.BinaryTools.Tests/Patch/RollingHashTests.cs @@ -11,6 +11,9 @@ public sealed class RollingHashTests [TestCase(1024, 64)] [TestCase(1024, 128)] [TestCase(1024, 256)] + [TestCase(1024, 512)] + [TestCase(1024 * 1024, 512)] + [TestCase(1024 * 1024, 1024)] public void Should_CalculateHash(int bufferLength, int bufferSize) { // Arrange diff --git a/src/BitSoft.BinaryTools/Patch/RollingHash.cs b/src/BitSoft.BinaryTools/Patch/RollingHash.cs index 6337fd7..87dc4c6 100644 --- a/src/BitSoft.BinaryTools/Patch/RollingHash.cs +++ b/src/BitSoft.BinaryTools/Patch/RollingHash.cs @@ -6,11 +6,11 @@ public struct RollingHash { private const uint Base = 65521; - private uint _a; - private uint _b; + private long _a; + private long _b; private readonly long _length; - private RollingHash(uint a, uint b, int length) + private RollingHash(long a, long b, int length) { _a = a; _b = b; @@ -19,8 +19,8 @@ private RollingHash(uint a, uint b, int length) public static RollingHash Create(ReadOnlySpan data) { - uint a = 1; - uint b = 0; + long a = 1; + long b = 0; foreach (var value in data) { @@ -33,12 +33,17 @@ public static RollingHash Create(ReadOnlySpan data) public void Update(byte removed, byte added) { - _a = (_a - removed + added) % Base; + // Use int for calculations within s1 update + var s1_new = _a - removed + added; + // Correct potential negative result back into positive range before modulo + _a = (s1_new % Base + Base) % Base; - var temp = _b - _length * removed + _a - 1; + // Use long for calculations within s2 update to handle large windowSize * byteOut + var tempS2 = _b - _length * removed + _a - 1; - _b = (uint)((temp % Base + Base) % Base); + // Correct potential negative result back into positive range before modulo + _b = (tempS2 % Base + Base) % Base; } - public uint GetChecksum() => (_b << 16) | _a; + public uint GetChecksum() => (uint) ((_b << 16) | _a); }