From a67fe38fe874e3a8e0a607a78ffea1f723e313f9 Mon Sep 17 00:00:00 2001 From: bitc0der <59016822+bitc0der@users.noreply.github.com> Date: Sat, 8 Nov 2025 20:50:57 +0700 Subject: [PATCH 01/20] Read original content from stream --- .../Patch/BinaryPatchTests.cs | 28 +++++----- .../Patch/BinaryPatchSource.cs | 51 ++++++++++++------- .../Patch/BlockInfoContainer.cs | 8 +-- .../Patch/PatchBlockInfo.cs | 12 ++--- 4 files changed, 55 insertions(+), 44 deletions(-) diff --git a/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs b/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs index cb4cee0..5d7a4f5 100644 --- a/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs +++ b/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Threading.Tasks; using BitSoft.BinaryTools.Patch; namespace BitSoft.BinaryTools.Tests.Patch; @@ -31,14 +32,15 @@ public void Should_DeserializePatch_FromStream() } [Test] - public void Should_ReturnBinaryPatchSegment_When_ModifiedSameLength() + public async Task Should_ReturnBinaryPatchSegment_When_ModifiedSameLength() { // Arrange var original = new byte[] { 0x0, 0x1, 0x0, 0x1, 0x0 }; var modified = new byte[] { 0x0, 0x0, 0x1, 0x0, 0x0 }; // Act - var patchSource = BinaryPatchSource.Create(original, blockSize: 2); + using var originalStream = new MemoryStream(original); + var patchSource = await BinaryPatchSource.CreateAsync(originalStream, blockSize: 2); var patch = patchSource.Calculate(modified); // Assert @@ -71,24 +73,24 @@ public void Should_ReturnBinaryPatchSegment_When_ModifiedSameLength() using var patchedStream = new MemoryStream(); BinaryPatchSource.Apply(original, patch, patchedStream); - + var patched = patchedStream.ToArray(); - + Assert.That(patched, Is.Not.Null); Assert.That(patched.Length, Is.EqualTo(modified.Length)); Assert.That(patched, Is.EqualTo(modified)); } [Test] - public void Should_ReturnEndOfFilePatchSegment_When_ModifiedShorterThanOriginal() + public async Task Should_ReturnEndOfFilePatchSegment_When_ModifiedShorterThanOriginal() { // Arrange var original = new byte[] { 0x0, 0x1 }; var modified = new byte[] { 0x0 }; // Act - var patchSource = BinaryPatchSource.Create(original, blockSize: 2); - + using var originalStream = new MemoryStream(original); + var patchSource = await BinaryPatchSource.CreateAsync(originalStream, blockSize: 2); var patch = patchSource.Calculate(modified); // Assert @@ -105,15 +107,15 @@ public void Should_ReturnEndOfFilePatchSegment_When_ModifiedShorterThanOriginal( } [Test] - public void Should_ReturnBinaryPatchSegment_When_ModifiedLongerThanOriginal() + public async Task Should_ReturnBinaryPatchSegment_When_ModifiedLongerThanOriginal() { // Arrange var original = new byte[] { 0x0 }; var modified = new byte[] { 0x0, 0x1 }; // Act - var patchSource = BinaryPatchSource.Create(original, blockSize: 2); - + using var originalStream = new MemoryStream(original); + var patchSource = await BinaryPatchSource.CreateAsync(originalStream, blockSize: 2); var patch = patchSource.Calculate(modified); // Assert @@ -132,15 +134,15 @@ public void Should_ReturnBinaryPatchSegment_When_ModifiedLongerThanOriginal() } [Test] - public void Should_ReturnBinaryPatchSegment_When_ModifiedLongerAndDifferent() + public async Task Should_ReturnBinaryPatchSegment_When_ModifiedLongerAndDifferent() { // Arrange var original = new byte[] { 0x0, 0x0 }; var modified = new byte[] { 0x0, 0x1, 0x0 }; // Act - var patchSource = BinaryPatchSource.Create(original); - + using var originalStream = new MemoryStream(original); + var patchSource = await BinaryPatchSource.CreateAsync(originalStream, blockSize: 2); var patch = patchSource.Calculate(modified); // Assert diff --git a/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs b/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs index 7deeb4d..8161c40 100644 --- a/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs +++ b/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs @@ -1,6 +1,9 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace BitSoft.BinaryTools.Patch; @@ -9,16 +12,19 @@ public sealed class BinaryPatchSource private readonly BlockInfoContainer _blockInfoContainer; private readonly int _blockSize; + private static readonly ArrayPool Pool = ArrayPool.Shared; + private BinaryPatchSource(BlockInfoContainer blockInfoContainer, int blockSize) { _blockInfoContainer = blockInfoContainer ?? throw new ArgumentNullException(nameof(blockInfoContainer)); _blockSize = blockSize; } - public static BinaryPatchSource Create(ReadOnlyMemory original, int blockSize = 4 * 1024) + public static async ValueTask CreateAsync(Stream original, int blockSize = 4 * 1024) { - var hashes = CalculateHashes(original, blockSize); - return new BinaryPatchSource(hashes, blockSize); + ArgumentNullException.ThrowIfNull(original); + var blockInfoContainer = await CalculateHashesAsync(original, blockSize); + return new BinaryPatchSource(blockInfoContainer, blockSize); } public BinaryPatch Calculate(ReadOnlyMemory modified) @@ -123,30 +129,39 @@ public static void Apply(ReadOnlyMemory original, BinaryPatch patch, Strea } } - private static BlockInfoContainer CalculateHashes(ReadOnlyMemory original, int blockSize) + private static async ValueTask CalculateHashesAsync( + Stream source, + int blockSize, + CancellationToken cancellationToken = default) { - var blockInfoContainer = new BlockInfoContainer(length: original.Length, blockSize: blockSize); + ArgumentNullException.ThrowIfNull(source); + + var blockInfoContainer = new BlockInfoContainer(); var blockIndex = 0; - while (true) + var buffer = Pool.Rent(blockSize); + try { - var offset = blockIndex * blockSize; - var left = original.Length - offset; - if (left == 0) - break; - var length = Math.Min(left, blockSize); - - var slice = original.Slice(start: offset, length: length); + while (true) + { + var length = await source.ReadAsync(buffer, offset: 0, count: blockSize, cancellationToken); + if (length == 0) + break; - var hash = RollingHash.Create(slice.Span); + var hash = RollingHash.Create(buffer); - blockInfoContainer.Process(hash: hash, blockIndex: blockIndex, blockLength: length); + blockInfoContainer.Process(hash: hash, blockIndex: blockIndex, blockLength: length); - if (length < blockSize) - break; + if (length < blockSize) + break; - blockIndex += 1; + blockIndex += 1; + } + } + finally + { + Pool.Return(buffer); } return blockInfoContainer; diff --git a/src/BitSoft.BinaryTools/Patch/BlockInfoContainer.cs b/src/BitSoft.BinaryTools/Patch/BlockInfoContainer.cs index cad095a..37c4269 100644 --- a/src/BitSoft.BinaryTools/Patch/BlockInfoContainer.cs +++ b/src/BitSoft.BinaryTools/Patch/BlockInfoContainer.cs @@ -4,13 +4,7 @@ namespace BitSoft.BinaryTools.Patch; internal sealed class BlockInfoContainer { - private readonly Dictionary> _hashes; - - public BlockInfoContainer(int length, int blockSize) - { - var capacity = length / blockSize; - _hashes = new Dictionary>(capacity: capacity); - } + private readonly Dictionary> _hashes = new(); public void Process(RollingHash hash, int blockIndex, int blockLength) { diff --git a/src/BitSoft.BinaryTools/Patch/PatchBlockInfo.cs b/src/BitSoft.BinaryTools/Patch/PatchBlockInfo.cs index 46c38bb..62a990b 100644 --- a/src/BitSoft.BinaryTools/Patch/PatchBlockInfo.cs +++ b/src/BitSoft.BinaryTools/Patch/PatchBlockInfo.cs @@ -2,16 +2,16 @@ namespace BitSoft.BinaryTools.Patch; public sealed class PatchBlockInfo { + public int BlockIndex { get; } + + public uint Hash { get; } + + public int Length { get; } + public PatchBlockInfo(int blockIndex, uint hash, int length) { BlockIndex = blockIndex; Hash = hash; Length = length; } - - public int BlockIndex { get; } - - public uint Hash { get; } - - public int Length { get; } } \ No newline at end of file From e5fcd484a013664ba69e8fe3a9f6eea3bc12f368 Mon Sep 17 00:00:00 2001 From: bitc0der <59016822+bitc0der@users.noreply.github.com> Date: Sat, 8 Nov 2025 21:00:51 +0700 Subject: [PATCH 02/20] Adjust buffer size --- src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs | 2 +- src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs b/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs index 5d7a4f5..7547711 100644 --- a/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs +++ b/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs @@ -142,7 +142,7 @@ public async Task Should_ReturnBinaryPatchSegment_When_ModifiedLongerAndDifferen // Act using var originalStream = new MemoryStream(original); - var patchSource = await BinaryPatchSource.CreateAsync(originalStream, blockSize: 2); + var patchSource = await BinaryPatchSource.CreateAsync(originalStream); var patch = patchSource.Calculate(modified); // Assert diff --git a/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs b/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs index 8161c40..867de48 100644 --- a/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs +++ b/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs @@ -43,8 +43,6 @@ public BinaryPatch Calculate(ReadOnlyMemory modified) while (position < modifiedSpan.Length) { - var checksum = rollingHash.GetChecksum(); - var block = _blockInfoContainer.Match(rollingHash); if (block is not null) @@ -136,6 +134,9 @@ private static async ValueTask CalculateHashesAsync( { ArgumentNullException.ThrowIfNull(source); + if (!source.CanRead) + throw new ArgumentException("source stream must be readable.", nameof(source)); + var blockInfoContainer = new BlockInfoContainer(); var blockIndex = 0; @@ -145,11 +146,11 @@ private static async ValueTask CalculateHashesAsync( { while (true) { - var length = await source.ReadAsync(buffer, offset: 0, count: blockSize, cancellationToken); + var length = await source.ReadAsync(buffer.AsMemory(start: 0, length: blockSize), cancellationToken); if (length == 0) break; - var hash = RollingHash.Create(buffer); + var hash = RollingHash.Create(buffer.AsSpan(start: 0, length: length)); blockInfoContainer.Process(hash: hash, blockIndex: blockIndex, blockLength: length); From 43fe32ccfb0cbca06bcca3d55577f94173e4fea1 Mon Sep 17 00:00:00 2001 From: bitc0der <59016822+bitc0der@users.noreply.github.com> Date: Sat, 8 Nov 2025 23:29:44 +0700 Subject: [PATCH 03/20] Calculate patch from stream --- .../Patch/BinaryPatchTests.cs | 25 ++-- .../Patch/BinaryPatchSource.cs | 136 +++++++++++------- 2 files changed, 101 insertions(+), 60 deletions(-) diff --git a/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs b/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs index 7547711..4ea8c57 100644 --- a/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs +++ b/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs @@ -38,10 +38,13 @@ public async Task Should_ReturnBinaryPatchSegment_When_ModifiedSameLength() var original = new byte[] { 0x0, 0x1, 0x0, 0x1, 0x0 }; var modified = new byte[] { 0x0, 0x0, 0x1, 0x0, 0x0 }; - // Act using var originalStream = new MemoryStream(original); + using var modifiedStream = new MemoryStream(modified); + + // Act + var patchSource = await BinaryPatchSource.CreateAsync(originalStream, blockSize: 2); - var patch = patchSource.Calculate(modified); + var patch = await patchSource.CalculateAsync(modifiedStream); // Assert Assert.That(patch, Is.Not.Null); @@ -88,10 +91,12 @@ public async Task Should_ReturnEndOfFilePatchSegment_When_ModifiedShorterThanOri var original = new byte[] { 0x0, 0x1 }; var modified = new byte[] { 0x0 }; - // Act using var originalStream = new MemoryStream(original); + using var modifiedStream = new MemoryStream(modified); + + // Act var patchSource = await BinaryPatchSource.CreateAsync(originalStream, blockSize: 2); - var patch = patchSource.Calculate(modified); + var patch = await patchSource.CalculateAsync(modifiedStream); // Assert Assert.That(patch, Is.Not.Null); @@ -113,10 +118,12 @@ public async Task Should_ReturnBinaryPatchSegment_When_ModifiedLongerThanOrigina var original = new byte[] { 0x0 }; var modified = new byte[] { 0x0, 0x1 }; - // Act using var originalStream = new MemoryStream(original); + using var modifiedStream = new MemoryStream(modified); + + // Act var patchSource = await BinaryPatchSource.CreateAsync(originalStream, blockSize: 2); - var patch = patchSource.Calculate(modified); + var patch = await patchSource.CalculateAsync(modifiedStream); // Assert Assert.That(patch, Is.Not.Null); @@ -140,10 +147,12 @@ public async Task Should_ReturnBinaryPatchSegment_When_ModifiedLongerAndDifferen var original = new byte[] { 0x0, 0x0 }; var modified = new byte[] { 0x0, 0x1, 0x0 }; - // Act using var originalStream = new MemoryStream(original); + using var modifiedStream = new MemoryStream(modified); + + // Act var patchSource = await BinaryPatchSource.CreateAsync(originalStream); - var patch = patchSource.Calculate(modified); + var patch = await patchSource.CalculateAsync(modifiedStream); // Assert Assert.That(patch, Is.Not.Null); diff --git a/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs b/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs index 867de48..607148b 100644 --- a/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs +++ b/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs @@ -27,77 +27,109 @@ public static async ValueTask CreateAsync(Stream original, in return new BinaryPatchSource(blockInfoContainer, blockSize); } - public BinaryPatch Calculate(ReadOnlyMemory modified) + public async ValueTask CalculateAsync(Stream modified, CancellationToken cancellationToken = default) { - var maxSize = modified.Length / _blockSize; - var segments = new List(capacity: maxSize); + ArgumentNullException.ThrowIfNull(modified); - var modifiedSpan = modified.Span; - var initialSpan = modified.Span[..Math.Min(modifiedSpan.Length, _blockSize)]; - var rollingHash = RollingHash.Create(initialSpan); + var segments = new List(); - const int NotDefined = -1; - - var segmentStart = NotDefined; - var position = 0; - - while (position < modifiedSpan.Length) + var bufferLength = _blockSize * 2; + var buffer = Pool.Rent(minimumLength: bufferLength); + try { - var block = _blockInfoContainer.Match(rollingHash); + var length = await modified.ReadAsync(buffer.AsMemory(start: 0, length: bufferLength), cancellationToken); + if (length == 0) + return new BinaryPatch(segments: [], _blockSize); - if (block is not null) - { - if (segmentStart != NotDefined) - { - var dataPatchSegment = new DataPatchSegment( - memory: modified.Slice(start: segmentStart, length: position - segmentStart) - ); - segments.Add(dataPatchSegment); - segmentStart = NotDefined; - } + const int NotDefined = -1; - var copyPatchSegment = new CopyPatchSegment(blockIndex: block.BlockIndex, length: block.Length); - segments.Add(copyPatchSegment); - position += block.Length; + var segmentStart = NotDefined; + var position = 0; - if (position >= modifiedSpan.Length) - break; + RollingHash rollingHash = default; + var resetHash = true; - var span = modifiedSpan.Slice( - start: position, - length: Math.Min(modifiedSpan.Length - position, _blockSize) - ); - rollingHash = RollingHash.Create(span); - } - else + while (true) { - if (segmentStart == NotDefined) - segmentStart = position; + while (position < length) + { + if (resetHash) + { + var spanLength = Math.Min(_blockSize, length); + var bufferSpan = buffer.AsSpan(start: 0, length: spanLength); + rollingHash = RollingHash.Create(bufferSpan); + resetHash = false; + } - position += 1; + var block = _blockInfoContainer.Match(rollingHash); - if (position == modifiedSpan.Length) - { - if (segmentStart != NotDefined) + if (block is null) { - var dataPatchSegment = new DataPatchSegment( - memory: modified.Slice(start: segmentStart, length: position - segmentStart) - ); - segments.Add(dataPatchSegment); + if (segmentStart == NotDefined) + segmentStart = position; + + position += 1; + + if (position == length) + { + if (segmentStart != NotDefined) + { + var dataPatchSegment = new DataPatchSegment( + memory: buffer.AsMemory(start: segmentStart, length: position - segmentStart) + ); + segments.Add(dataPatchSegment); + } + + break; + } + + var removedByte = buffer[position - 1]; + var addedByte = position + _blockSize <= length + ? buffer[position + _blockSize - 1] + : buffer[buffer.Length - 1]; + + rollingHash.Update(removed: removedByte, added: addedByte); } + else + { + if (segmentStart != NotDefined) + { + var dataPatchSegment = new DataPatchSegment( + memory: buffer.AsMemory(start: segmentStart, length: position - segmentStart) + ); + segments.Add(dataPatchSegment); + segmentStart = NotDefined; + } - break; + var copyPatchSegment = new CopyPatchSegment(blockIndex: block.BlockIndex, length: block.Length); + segments.Add(copyPatchSegment); + + buffer + .AsSpan(start: position + block.Length, length: bufferLength - position - block.Length - 1) + .CopyTo(buffer.AsSpan(start: 0)); + + resetHash = true; + + break; + } } - var removedByte = modifiedSpan[position - 1]; - var addedByte = - position + _blockSize <= modifiedSpan.Length - ? modifiedSpan[position + _blockSize - 1] - : modifiedSpan[modifiedSpan.Length - 1]; + length = await modified.ReadAsync( + buffer.AsMemory(start: position, length: bufferLength - position - 1), + cancellationToken: cancellationToken + ); + + if (length == 0) + break; - rollingHash.Update(removed: removedByte, added: addedByte); + length += position; + position = 0; } } + finally + { + Pool.Return(buffer); + } return new BinaryPatch(segments, _blockSize); } From 9e23f9ff9e55c2347fbe30979886997b3396f079 Mon Sep 17 00:00:00 2001 From: bitc0der <59016822+bitc0der@users.noreply.github.com> Date: Sat, 15 Nov 2025 21:00:03 +0700 Subject: [PATCH 04/20] Support buffer wraparound --- .../Patch/BinaryPatchTests.cs | 29 +++++++---- .../Patch/BinaryPatchSource.cs | 48 ++++++++++++++----- 2 files changed, 55 insertions(+), 22 deletions(-) diff --git a/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs b/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs index 4ea8c57..ad1060c 100644 --- a/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs +++ b/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs @@ -116,7 +116,7 @@ public async Task Should_ReturnBinaryPatchSegment_When_ModifiedLongerThanOrigina { // Arrange var original = new byte[] { 0x0 }; - var modified = new byte[] { 0x0, 0x1 }; + var modified = new byte[] { 0x1, 0x2 }; using var originalStream = new MemoryStream(original); using var modifiedStream = new MemoryStream(modified); @@ -144,28 +144,37 @@ public async Task Should_ReturnBinaryPatchSegment_When_ModifiedLongerThanOrigina public async Task Should_ReturnBinaryPatchSegment_When_ModifiedLongerAndDifferent() { // Arrange - var original = new byte[] { 0x0, 0x0 }; - var modified = new byte[] { 0x0, 0x1, 0x0 }; + var original = new byte[] { 0x1, 0x2 }; + var modified = new byte[] { 0x3, 0x4, 0x5 }; using var originalStream = new MemoryStream(original); using var modifiedStream = new MemoryStream(modified); // Act - var patchSource = await BinaryPatchSource.CreateAsync(originalStream); + var patchSource = await BinaryPatchSource.CreateAsync(originalStream, blockSize: 2); var patch = await patchSource.CalculateAsync(modifiedStream); // Assert Assert.That(patch, Is.Not.Null); Assert.That(patch.Segments, Is.Not.Empty); - Assert.That(patch.Segments.Count, Is.EqualTo(1)); + Assert.That(patch.Segments.Count, Is.EqualTo(2)); - var firstSegment = patch.Segments[0]; + var segment = patch.Segments[0]; - Assert.That(firstSegment, Is.Not.Null); + Assert.That(segment, Is.Not.Null); - var binaryPatchSegment = firstSegment as DataPatchSegment; + var dataPatchSegment = segment as DataPatchSegment; - Assert.That(binaryPatchSegment, Is.Not.Null); - Assert.That(binaryPatchSegment.Memory.Length, Is.EqualTo(3)); + Assert.That(dataPatchSegment, Is.Not.Null); + Assert.That(dataPatchSegment.Memory.Length, Is.EqualTo(2)); + + segment = patch.Segments[1]; + + Assert.That(segment, Is.Not.Null); + + dataPatchSegment = segment as DataPatchSegment; + + Assert.That(dataPatchSegment, Is.Not.Null); + Assert.That(dataPatchSegment.Memory.Length, Is.EqualTo(1)); } } \ No newline at end of file diff --git a/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs b/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs index 607148b..368ae52 100644 --- a/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs +++ b/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs @@ -65,21 +65,45 @@ public async ValueTask CalculateAsync(Stream modified, Cancellation if (block is null) { - if (segmentStart == NotDefined) + if (length == _blockSize) + { + var dataPatchSegment = new DataPatchSegment( + memory: buffer.AsMemory(start: position, length: _blockSize) + ); + segments.Add(dataPatchSegment); + position = 0; + break; + } + else if (segmentStart == NotDefined) + { segmentStart = position; + } + else if (position - segmentStart + 1 == _blockSize) + { + var dataPatchSegment = new DataPatchSegment( + memory: buffer.AsMemory(start: segmentStart, length: position - segmentStart + 1) + ); + segments.Add(dataPatchSegment); + + buffer + .AsSpan(start: position + 1, length: bufferLength - position - 2) + .CopyTo(buffer.AsSpan(start: 0)); + + segmentStart = NotDefined; + resetHash = true; + + break; + } position += 1; if (position == length) { - if (segmentStart != NotDefined) - { - var dataPatchSegment = new DataPatchSegment( - memory: buffer.AsMemory(start: segmentStart, length: position - segmentStart) - ); - segments.Add(dataPatchSegment); - } - + var dataPatchSegment = new DataPatchSegment( + memory: buffer.AsMemory(start: segmentStart, length: position - segmentStart) + ); + segments.Add(dataPatchSegment); + position = 0; break; } @@ -119,11 +143,11 @@ public async ValueTask CalculateAsync(Stream modified, Cancellation cancellationToken: cancellationToken ); - if (length == 0) - break; - length += position; position = 0; + + if (length == 0) + break; } } finally From 18a55415706d1038b11e0828aeb041f138c471d6 Mon Sep 17 00:00:00 2001 From: bitc0der <59016822+bitc0der@users.noreply.github.com> Date: Sat, 15 Nov 2025 21:01:58 +0700 Subject: [PATCH 05/20] Support short buffer length --- src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs b/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs index 368ae52..ade1a9f 100644 --- a/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs +++ b/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs @@ -65,16 +65,17 @@ public async ValueTask CalculateAsync(Stream modified, Cancellation if (block is null) { - if (length == _blockSize) + if (length <= _blockSize) { var dataPatchSegment = new DataPatchSegment( - memory: buffer.AsMemory(start: position, length: _blockSize) + memory: buffer.AsMemory(start: position, length: length) ); segments.Add(dataPatchSegment); position = 0; break; } - else if (segmentStart == NotDefined) + + if (segmentStart == NotDefined) { segmentStart = position; } From 6eaa05bbbd983e6e40f64eb86fcaa4f776f00170 Mon Sep 17 00:00:00 2001 From: bitc0der <59016822+bitc0der@users.noreply.github.com> Date: Tue, 25 Nov 2025 17:19:35 +0700 Subject: [PATCH 06/20] Support short last block --- .../Patch/BinaryPatchSource.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs b/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs index ade1a9f..faed9b6 100644 --- a/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs +++ b/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs @@ -108,12 +108,16 @@ public async ValueTask CalculateAsync(Stream modified, Cancellation break; } - var removedByte = buffer[position - 1]; - var addedByte = position + _blockSize <= length - ? buffer[position + _blockSize - 1] - : buffer[buffer.Length - 1]; - - rollingHash.Update(removed: removedByte, added: addedByte); + if (position + _blockSize < length) + { + var removedByte = buffer[position - 1]; + var addedByte = buffer[position + _blockSize - 1]; + rollingHash.Update(removed: removedByte, added: addedByte); + } + else + { + resetHash = true; + } } else { From f7691ed2594c03aca3afce5877d59080663bcada Mon Sep 17 00:00:00 2001 From: bitc0der <59016822+bitc0der@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:38:14 +0700 Subject: [PATCH 07/20] Create and apply directly to stream --- .../Patch/BinaryPatchTests.cs | 125 ++++++------------ src/BitSoft.BinaryTools/Patch/BinaryPatch.cs | 120 ----------------- .../Patch/BinaryPatchSource.cs | 87 +++++++----- .../Patch/CopyPatchSegment.cs | 14 -- .../Patch/DataPatchSegment.cs | 13 -- .../Patch/IBinaryPatchSegment.cs | 6 - src/BitSoft.BinaryTools/Patch/PatchReader.cs | 100 ++++++++++++++ src/BitSoft.BinaryTools/Patch/PatchWriter.cs | 56 ++++++++ .../Patch/ProtocolConst.cs | 6 +- 9 files changed, 252 insertions(+), 275 deletions(-) delete mode 100644 src/BitSoft.BinaryTools/Patch/BinaryPatch.cs delete mode 100644 src/BitSoft.BinaryTools/Patch/CopyPatchSegment.cs delete mode 100644 src/BitSoft.BinaryTools/Patch/DataPatchSegment.cs delete mode 100644 src/BitSoft.BinaryTools/Patch/IBinaryPatchSegment.cs create mode 100644 src/BitSoft.BinaryTools/Patch/PatchReader.cs create mode 100644 src/BitSoft.BinaryTools/Patch/PatchWriter.cs diff --git a/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs b/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs index ad1060c..fd1b870 100644 --- a/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs +++ b/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs @@ -7,30 +7,6 @@ namespace BitSoft.BinaryTools.Tests.Patch; [TestFixture] public class BinaryPatchTests { - [Test] - public void Should_DeserializePatch_FromStream() - { - // Arrange - var data = new byte[] { 0x0, 0x1, 0x0, 0x1, 0x0 }; - var sourcePatch = new BinaryPatch(segments: - [ - new CopyPatchSegment(blockIndex: 5, length: 34), - new DataPatchSegment(memory: data) - ], blockSize: 1024); - - // Act - using var patchStream = new MemoryStream(); - sourcePatch.Write(patchStream); - patchStream.Position = 0; - var restoredPatch = BinaryPatch.Read(patchStream); - - // Assert - Assert.That(restoredPatch, Is.Not.Null); - Assert.That(restoredPatch.BlockSize, Is.EqualTo(sourcePatch.BlockSize)); - Assert.That(restoredPatch.Segments, Is.Not.Empty); - Assert.That(restoredPatch.Segments.Count, Is.EqualTo(sourcePatch.Segments.Count)); - } - [Test] public async Task Should_ReturnBinaryPatchSegment_When_ModifiedSameLength() { @@ -40,42 +16,19 @@ public async Task Should_ReturnBinaryPatchSegment_When_ModifiedSameLength() using var originalStream = new MemoryStream(original); using var modifiedStream = new MemoryStream(modified); + using var patchStream = new MemoryStream(); // Act - var patchSource = await BinaryPatchSource.CreateAsync(originalStream, blockSize: 2); - var patch = await patchSource.CalculateAsync(modifiedStream); + await patchSource.CalculateAsync(modifiedStream, output: patchStream); // Assert - Assert.That(patch, Is.Not.Null); - Assert.That(patch.Segments, Is.Not.Empty); - Assert.That(patch.Segments.Count, Is.EqualTo(3)); - - var segment = patch.Segments[0]; - - Assert.That(segment, Is.Not.Null); - var binaryPatchSegment = segment as DataPatchSegment; - Assert.That(binaryPatchSegment, Is.Not.Null); - Assert.That(binaryPatchSegment.Memory.Length, Is.EqualTo(1)); - - segment = patch.Segments[1]; - - Assert.That(segment, Is.Not.Null); - var copyPatchSegment = segment as CopyPatchSegment; - Assert.That(copyPatchSegment, Is.Not.Null); - Assert.That(copyPatchSegment.BlockIndex, Is.EqualTo(0)); - Assert.That(copyPatchSegment.Length, Is.EqualTo(2)); - - segment = patch.Segments[2]; - - Assert.That(segment, Is.Not.Null); - binaryPatchSegment = segment as DataPatchSegment; - Assert.That(binaryPatchSegment, Is.Not.Null); - Assert.That(binaryPatchSegment.Memory.Length, Is.EqualTo(2)); + originalStream.Position = 0; + patchStream.Position = 0; using var patchedStream = new MemoryStream(); - BinaryPatchSource.Apply(original, patch, patchedStream); + await BinaryPatchSource.ApplyAsync(source: originalStream, patch: patchStream, output: patchedStream); var patched = patchedStream.ToArray(); @@ -93,22 +46,25 @@ public async Task Should_ReturnEndOfFilePatchSegment_When_ModifiedShorterThanOri using var originalStream = new MemoryStream(original); using var modifiedStream = new MemoryStream(modified); + using var patchStream = new MemoryStream(); // Act var patchSource = await BinaryPatchSource.CreateAsync(originalStream, blockSize: 2); - var patch = await patchSource.CalculateAsync(modifiedStream); + await patchSource.CalculateAsync(modifiedStream, output: patchStream); // Assert - Assert.That(patch, Is.Not.Null); - Assert.That(patch.Segments, Is.Not.Empty); - Assert.That(patch.Segments.Count, Is.EqualTo(1)); + originalStream.Position = 0; + patchStream.Position = 0; + + using var patchedStream = new MemoryStream(); + + await BinaryPatchSource.ApplyAsync(source: originalStream, patch: patchStream, output: patchedStream); - var segment = patch.Segments[0]; + var patched = patchedStream.ToArray(); - Assert.That(segment, Is.Not.Null); - var binaryPatchSegment = segment as DataPatchSegment; - Assert.That(binaryPatchSegment, Is.Not.Null); - Assert.That(binaryPatchSegment.Memory.Length, Is.EqualTo(1)); + Assert.That(patched, Is.Not.Null); + Assert.That(patched.Length, Is.EqualTo(modified.Length)); + Assert.That(patched, Is.EqualTo(modified)); } [Test] @@ -120,24 +76,25 @@ public async Task Should_ReturnBinaryPatchSegment_When_ModifiedLongerThanOrigina using var originalStream = new MemoryStream(original); using var modifiedStream = new MemoryStream(modified); + using var patchStream = new MemoryStream(); // Act var patchSource = await BinaryPatchSource.CreateAsync(originalStream, blockSize: 2); - var patch = await patchSource.CalculateAsync(modifiedStream); + await patchSource.CalculateAsync(modifiedStream, output: patchStream); // Assert - Assert.That(patch, Is.Not.Null); - Assert.That(patch.Segments, Is.Not.Empty); - Assert.That(patch.Segments.Count, Is.EqualTo(1)); + originalStream.Position = 0; + patchStream.Position = 0; - var firstSegment = patch.Segments[0]; + using var patchedStream = new MemoryStream(); - Assert.That(firstSegment, Is.Not.Null); + await BinaryPatchSource.ApplyAsync(source: originalStream, patch: patchStream, output: patchedStream); - var binaryPatchSegment = firstSegment as DataPatchSegment; + var patched = patchedStream.ToArray(); - Assert.That(binaryPatchSegment, Is.Not.Null); - Assert.That(binaryPatchSegment.Memory.Length, Is.EqualTo(2)); + Assert.That(patched, Is.Not.Null); + Assert.That(patched.Length, Is.EqualTo(modified.Length)); + Assert.That(patched, Is.EqualTo(modified)); } [Test] @@ -149,32 +106,24 @@ public async Task Should_ReturnBinaryPatchSegment_When_ModifiedLongerAndDifferen using var originalStream = new MemoryStream(original); using var modifiedStream = new MemoryStream(modified); + using var patchStream = new MemoryStream(); // Act var patchSource = await BinaryPatchSource.CreateAsync(originalStream, blockSize: 2); - var patch = await patchSource.CalculateAsync(modifiedStream); + await patchSource.CalculateAsync(modifiedStream, output: patchStream); // Assert - Assert.That(patch, Is.Not.Null); - Assert.That(patch.Segments, Is.Not.Empty); - Assert.That(patch.Segments.Count, Is.EqualTo(2)); - - var segment = patch.Segments[0]; - - Assert.That(segment, Is.Not.Null); - - var dataPatchSegment = segment as DataPatchSegment; + originalStream.Position = 0; + patchStream.Position = 0; - Assert.That(dataPatchSegment, Is.Not.Null); - Assert.That(dataPatchSegment.Memory.Length, Is.EqualTo(2)); - - segment = patch.Segments[1]; + using var patchedStream = new MemoryStream(); - Assert.That(segment, Is.Not.Null); + await BinaryPatchSource.ApplyAsync(source: originalStream, patch: patchStream, output: patchedStream); - dataPatchSegment = segment as DataPatchSegment; + var patched = patchedStream.ToArray(); - Assert.That(dataPatchSegment, Is.Not.Null); - Assert.That(dataPatchSegment.Memory.Length, Is.EqualTo(1)); + Assert.That(patched, Is.Not.Null); + Assert.That(patched.Length, Is.EqualTo(modified.Length)); + Assert.That(patched, Is.EqualTo(modified)); } } \ No newline at end of file diff --git a/src/BitSoft.BinaryTools/Patch/BinaryPatch.cs b/src/BitSoft.BinaryTools/Patch/BinaryPatch.cs deleted file mode 100644 index fc9a4cd..0000000 --- a/src/BitSoft.BinaryTools/Patch/BinaryPatch.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace BitSoft.BinaryTools.Patch; - -public sealed class BinaryPatch -{ - public int BlockSize { get; } - - public IReadOnlyList Segments { get; } - - public static Encoding DefaultEncoding => Encoding.UTF8; - - public BinaryPatch(IReadOnlyList segments, int blockSize) - { - if (blockSize <= 0) throw new ArgumentOutOfRangeException(nameof(blockSize)); - - Segments = segments ?? throw new ArgumentNullException(nameof(segments)); - BlockSize = blockSize; - } - - public void Write(Stream target, Encoding? encoding = null) - { - ArgumentNullException.ThrowIfNull(target); - - if (!target.CanWrite) - throw new ArgumentException("The target stream must be writable.", nameof(target)); - - encoding ??= DefaultEncoding; - - using var binaryWriter = new BinaryWriter(target, encoding, leaveOpen: true); - - binaryWriter.Write(ProtocolConst.ProtocolVersion); - binaryWriter.Write(BlockSize); - - for (var i = 0; i < Segments.Count; i++) - { - var segment = Segments[i]; - - var segmentType = segment switch - { - CopyPatchSegment => ProtocolConst.SegmentTypes.CopyPatchSegment, - DataPatchSegment => ProtocolConst.SegmentTypes.DataPatchSegment, - _ => throw new InvalidOperationException($"Invalid segment type '{segment.GetType()}'.") - }; - binaryWriter.Write(segmentType); - - switch (segment) - { - case CopyPatchSegment copyPatchSegment: - binaryWriter.Write(copyPatchSegment.BlockIndex); - binaryWriter.Write(copyPatchSegment.Length); - break; - case DataPatchSegment dataPatchSegment: - binaryWriter.Write(dataPatchSegment.Memory.Length); - binaryWriter.Write(dataPatchSegment.Memory.Span); - break; - } - } - - binaryWriter.Write(ProtocolConst.SegmentTypes.EndPatchSegment); - } - - public static BinaryPatch Read(Stream source, Encoding? encoding = null) - { - ArgumentNullException.ThrowIfNull(source); - - if (!source.CanRead) - throw new ArgumentException("The source stream must be readable.", nameof(source)); - - encoding ??= DefaultEncoding; - - using var binaryReader = new BinaryReader(source, encoding, leaveOpen: true); - - var protocolVersion = binaryReader.ReadInt32(); - if (protocolVersion > ProtocolConst.ProtocolVersion) - throw new InvalidOperationException($"Invalid protocol version '{protocolVersion}'."); - - var blockSize = binaryReader.ReadInt32(); - - var segments = new List(); - - while (true) - { - var segmentType = binaryReader.ReadByte(); - IBinaryPatchSegment? segment = null; - switch (segmentType) - { - case ProtocolConst.SegmentTypes.CopyPatchSegment: - { - var blockIndex = binaryReader.ReadInt32(); - var length = binaryReader.ReadInt32(); - segment = new CopyPatchSegment(blockIndex: blockIndex, length: length); - break; - } - case ProtocolConst.SegmentTypes.DataPatchSegment: - { - var length = binaryReader.ReadInt32(); - var bytes = binaryReader.ReadBytes(length); - segment = new DataPatchSegment(bytes); - break; - } - case ProtocolConst.SegmentTypes.EndPatchSegment: - // do nothing - break; - default: - throw new InvalidOperationException($"Invalid segment type Id '{segmentType}'."); - } - - if (segment is not null) - segments.Add(segment); - else - break; - } - - return new BinaryPatch(segments, blockSize); - } -} \ No newline at end of file diff --git a/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs b/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs index faed9b6..9eff813 100644 --- a/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs +++ b/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs @@ -1,6 +1,5 @@ using System; using System.Buffers; -using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -27,11 +26,14 @@ public static async ValueTask CreateAsync(Stream original, in return new BinaryPatchSource(blockInfoContainer, blockSize); } - public async ValueTask CalculateAsync(Stream modified, CancellationToken cancellationToken = default) + public async ValueTask CalculateAsync(Stream modified, Stream output, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(modified); + ArgumentNullException.ThrowIfNull(output); - var segments = new List(); + using var writer = new PatchWriter(output); + + await writer.WriteHeaderAsync(blockSize: _blockSize, cancellationToken); var bufferLength = _blockSize * 2; var buffer = Pool.Rent(minimumLength: bufferLength); @@ -39,7 +41,7 @@ public async ValueTask CalculateAsync(Stream modified, Cancellation { var length = await modified.ReadAsync(buffer.AsMemory(start: 0, length: bufferLength), cancellationToken); if (length == 0) - return new BinaryPatch(segments: [], _blockSize); + return; const int NotDefined = -1; @@ -67,10 +69,8 @@ public async ValueTask CalculateAsync(Stream modified, Cancellation { if (length <= _blockSize) { - var dataPatchSegment = new DataPatchSegment( - memory: buffer.AsMemory(start: position, length: length) - ); - segments.Add(dataPatchSegment); + var memory = buffer.AsMemory(start: position, length: length); + await writer.WriteDataAsync(memory, cancellationToken); position = 0; break; } @@ -81,10 +81,8 @@ public async ValueTask CalculateAsync(Stream modified, Cancellation } else if (position - segmentStart + 1 == _blockSize) { - var dataPatchSegment = new DataPatchSegment( - memory: buffer.AsMemory(start: segmentStart, length: position - segmentStart + 1) - ); - segments.Add(dataPatchSegment); + var memory = buffer.AsMemory(start: segmentStart, length: position - segmentStart + 1); + await writer.WriteDataAsync(memory, cancellationToken); buffer .AsSpan(start: position + 1, length: bufferLength - position - 2) @@ -100,10 +98,8 @@ public async ValueTask CalculateAsync(Stream modified, Cancellation if (position == length) { - var dataPatchSegment = new DataPatchSegment( - memory: buffer.AsMemory(start: segmentStart, length: position - segmentStart) - ); - segments.Add(dataPatchSegment); + var memory = buffer.AsMemory(start: segmentStart, length: position - segmentStart); + await writer.WriteDataAsync(memory, cancellationToken); position = 0; break; } @@ -123,15 +119,16 @@ public async ValueTask CalculateAsync(Stream modified, Cancellation { if (segmentStart != NotDefined) { - var dataPatchSegment = new DataPatchSegment( - memory: buffer.AsMemory(start: segmentStart, length: position - segmentStart) - ); - segments.Add(dataPatchSegment); + var memory = buffer.AsMemory(start: segmentStart, length: position - segmentStart); + await writer.WriteDataAsync(memory, cancellationToken); segmentStart = NotDefined; } - var copyPatchSegment = new CopyPatchSegment(blockIndex: block.BlockIndex, length: block.Length); - segments.Add(copyPatchSegment); + await writer.WriteCopyAsync( + blockIndex: block.BlockIndex, + blockLength: block.Length, + cancellationToken: cancellationToken + ); buffer .AsSpan(start: position + block.Length, length: bufferLength - position - block.Length - 1) @@ -160,27 +157,51 @@ public async ValueTask CalculateAsync(Stream modified, Cancellation Pool.Return(buffer); } - return new BinaryPatch(segments, _blockSize); + await writer.CompleteAsync(cancellationToken); } - public static void Apply(ReadOnlyMemory original, BinaryPatch patch, Stream target) + public static async ValueTask ApplyAsync( + Stream source, + Stream patch, + Stream output, + CancellationToken cancellationToken = default) { + ArgumentNullException.ThrowIfNull(source); ArgumentNullException.ThrowIfNull(patch); - ArgumentNullException.ThrowIfNull(target); + ArgumentNullException.ThrowIfNull(output); + + if (!source.CanRead) + throw new ArgumentException("source stream must be readable.", nameof(source)); + if (!source.CanSeek) + throw new ArgumentException("source stream must be seekable.", nameof(source)); - foreach (var segment in patch.Segments) + using var reader = new PatchReader(patch); + var blockSize = await reader.InitializeAsync(cancellationToken); + + while (await reader.ReadAsync(cancellationToken)) { - switch (segment) + switch (reader.Segment) { case DataPatchSegment dataPatchSegment: - target.Write(dataPatchSegment.Memory.Span); + await output.WriteAsync(dataPatchSegment.Data, cancellationToken); break; case CopyPatchSegment copyPatchSegment: - var slice = original.Slice( - start: copyPatchSegment.BlockIndex * patch.BlockSize, - length: copyPatchSegment.Length - ); - target.Write(slice.Span); + var targetPosition = blockSize * copyPatchSegment.BlockIndex; + source.Seek(targetPosition, SeekOrigin.Begin); + var buffer = Pool.Rent(copyPatchSegment.BlockLength); + try + { + var count = await source.ReadAsync(buffer, + offset: 0, + count: copyPatchSegment.BlockLength, + cancellationToken); + await output.WriteAsync(buffer, offset: 0, count: count, cancellationToken); + } + finally + { + Pool.Return(buffer); + } + break; default: throw new NotSupportedException(); diff --git a/src/BitSoft.BinaryTools/Patch/CopyPatchSegment.cs b/src/BitSoft.BinaryTools/Patch/CopyPatchSegment.cs deleted file mode 100644 index e9aaa6a..0000000 --- a/src/BitSoft.BinaryTools/Patch/CopyPatchSegment.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace BitSoft.BinaryTools.Patch; - -public sealed class CopyPatchSegment : IBinaryPatchSegment -{ - public int BlockIndex { get; } - - public int Length { get; } - - public CopyPatchSegment(int blockIndex, int length) - { - BlockIndex = blockIndex; - Length = length; - } -} \ No newline at end of file diff --git a/src/BitSoft.BinaryTools/Patch/DataPatchSegment.cs b/src/BitSoft.BinaryTools/Patch/DataPatchSegment.cs deleted file mode 100644 index 32782dc..0000000 --- a/src/BitSoft.BinaryTools/Patch/DataPatchSegment.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace BitSoft.BinaryTools.Patch; - -public sealed class DataPatchSegment : IBinaryPatchSegment -{ - public ReadOnlyMemory Memory { get; } - - public DataPatchSegment(ReadOnlyMemory memory) - { - Memory = memory; - } -} \ No newline at end of file diff --git a/src/BitSoft.BinaryTools/Patch/IBinaryPatchSegment.cs b/src/BitSoft.BinaryTools/Patch/IBinaryPatchSegment.cs deleted file mode 100644 index 6b6fa71..0000000 --- a/src/BitSoft.BinaryTools/Patch/IBinaryPatchSegment.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace BitSoft.BinaryTools.Patch; - -public interface IBinaryPatchSegment -{ - -} \ No newline at end of file diff --git a/src/BitSoft.BinaryTools/Patch/PatchReader.cs b/src/BitSoft.BinaryTools/Patch/PatchReader.cs new file mode 100644 index 0000000..bc2baf1 --- /dev/null +++ b/src/BitSoft.BinaryTools/Patch/PatchReader.cs @@ -0,0 +1,100 @@ +using System; +using System.Buffers; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace BitSoft.BinaryTools.Patch; + +internal interface IPatchSegment +{ +} + +internal sealed class DataPatchSegment : IPatchSegment +{ + public DataPatchSegment(ReadOnlyMemory data) + { + Data = data; + } + + public ReadOnlyMemory Data { get; } +} + +internal sealed class CopyPatchSegment : IPatchSegment +{ + public int BlockIndex { get; } + + public int BlockLength { get; } + + public CopyPatchSegment(int blockIndex, int blockLength) + { + BlockIndex = blockIndex; + BlockLength = blockLength; + } +} + +internal sealed class PatchReader : IDisposable +{ + private readonly Stream _source; + private readonly BinaryReader _reader; + + private static readonly ArrayPool Pool = ArrayPool.Shared; + private byte[]? _buffer; + + public IPatchSegment? Segment { get; private set; } = null; + + public PatchReader(Stream source) + { + _source = source ?? throw new ArgumentNullException(nameof(source)); + _reader = new BinaryReader(source, encoding: ProtocolConst.DefaultEncoding, leaveOpen: true); + } + + public ValueTask InitializeAsync(CancellationToken cancellationToken) + { + var protocolVersion = _reader.ReadInt32(); + + if (protocolVersion > ProtocolConst.ProtocolVersion) + throw new InvalidOperationException($"Invalid protocol version '{protocolVersion}'."); + + var blockSize = _reader.ReadInt32(); + + _buffer = Pool.Rent(blockSize); + + return ValueTask.FromResult(blockSize); + } + + public ValueTask ReadAsync(CancellationToken cancellationToken) + { + var segmentType = _reader.ReadByte(); + + switch (segmentType) + { + case ProtocolConst.SegmentTypes.EndPatchSegment: + Segment = null; + return ValueTask.FromResult(false); + case ProtocolConst.SegmentTypes.CopyPatchSegment: + var blockIndex = _reader.ReadInt32(); + var blockLength = _reader.ReadInt32(); + Segment = new CopyPatchSegment(blockIndex: blockIndex, blockLength: blockLength); + break; + case ProtocolConst.SegmentTypes.DataPatchSegment: + var length = _reader.ReadInt32(); + var span = _buffer.AsSpan(start: 0, length: length); + var count = _reader.Read(span); + Segment = new DataPatchSegment(data: _buffer.AsMemory(start: 0, length: count)); + break; + default: + throw new ArgumentOutOfRangeException(nameof(segmentType), segmentType, null); + } + + return ValueTask.FromResult(true); + } + + public void Dispose() + { + _reader.Dispose(); + + if (_buffer is not null) + Pool.Return(_buffer); + } +} \ No newline at end of file diff --git a/src/BitSoft.BinaryTools/Patch/PatchWriter.cs b/src/BitSoft.BinaryTools/Patch/PatchWriter.cs new file mode 100644 index 0000000..913dbd3 --- /dev/null +++ b/src/BitSoft.BinaryTools/Patch/PatchWriter.cs @@ -0,0 +1,56 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace BitSoft.BinaryTools.Patch; + +internal sealed class PatchWriter : IDisposable +{ + private readonly Stream _output; + + private readonly BinaryWriter _writer; + + public PatchWriter(Stream output) + { + _output = output ?? throw new ArgumentNullException(nameof(output)); + _writer = new BinaryWriter(_output, encoding: ProtocolConst.DefaultEncoding, leaveOpen: true); + } + + public ValueTask WriteHeaderAsync(int blockSize, CancellationToken cancellationToken) + { + _writer.Write(ProtocolConst.ProtocolVersion); + _writer.Write(blockSize); + + return ValueTask.CompletedTask; + } + + public ValueTask WriteDataAsync(ReadOnlyMemory memory, CancellationToken cancellationToken) + { + _writer.Write(ProtocolConst.SegmentTypes.DataPatchSegment); + _writer.Write(memory.Length); + _writer.Write(memory.Span); + + return ValueTask.CompletedTask; + } + + public ValueTask WriteCopyAsync(int blockIndex, int blockLength, CancellationToken cancellationToken) + { + _writer.Write(ProtocolConst.SegmentTypes.CopyPatchSegment); + _writer.Write(blockIndex); + _writer.Write(blockLength); + + return ValueTask.CompletedTask; + } + + public ValueTask CompleteAsync(CancellationToken cancellationToken) + { + _writer.Write(ProtocolConst.SegmentTypes.EndPatchSegment); + return ValueTask.CompletedTask; + } + + public void Dispose() + { + _writer.Dispose(); + } +} \ No newline at end of file diff --git a/src/BitSoft.BinaryTools/Patch/ProtocolConst.cs b/src/BitSoft.BinaryTools/Patch/ProtocolConst.cs index b1664b0..02bae57 100644 --- a/src/BitSoft.BinaryTools/Patch/ProtocolConst.cs +++ b/src/BitSoft.BinaryTools/Patch/ProtocolConst.cs @@ -1,9 +1,13 @@ +using System.Text; + namespace BitSoft.BinaryTools.Patch; public static class ProtocolConst { + public static Encoding DefaultEncoding => Encoding.UTF8; + public const int ProtocolVersion = 1; - + public static class SegmentTypes { public const byte CopyPatchSegment = 0x1; From 56ad6c22ec984f55f55741429c3c294c594f1cd4 Mon Sep 17 00:00:00 2001 From: bitc0der <59016822+bitc0der@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:41:02 +0700 Subject: [PATCH 08/20] Rename method --- src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs | 8 ++++---- src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs b/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs index fd1b870..d9067de 100644 --- a/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs +++ b/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs @@ -20,7 +20,7 @@ public async Task Should_ReturnBinaryPatchSegment_When_ModifiedSameLength() // Act var patchSource = await BinaryPatchSource.CreateAsync(originalStream, blockSize: 2); - await patchSource.CalculateAsync(modifiedStream, output: patchStream); + await patchSource.CreateAsync(modifiedStream, output: patchStream); // Assert originalStream.Position = 0; @@ -50,7 +50,7 @@ public async Task Should_ReturnEndOfFilePatchSegment_When_ModifiedShorterThanOri // Act var patchSource = await BinaryPatchSource.CreateAsync(originalStream, blockSize: 2); - await patchSource.CalculateAsync(modifiedStream, output: patchStream); + await patchSource.CreateAsync(modifiedStream, output: patchStream); // Assert originalStream.Position = 0; @@ -80,7 +80,7 @@ public async Task Should_ReturnBinaryPatchSegment_When_ModifiedLongerThanOrigina // Act var patchSource = await BinaryPatchSource.CreateAsync(originalStream, blockSize: 2); - await patchSource.CalculateAsync(modifiedStream, output: patchStream); + await patchSource.CreateAsync(modifiedStream, output: patchStream); // Assert originalStream.Position = 0; @@ -110,7 +110,7 @@ public async Task Should_ReturnBinaryPatchSegment_When_ModifiedLongerAndDifferen // Act var patchSource = await BinaryPatchSource.CreateAsync(originalStream, blockSize: 2); - await patchSource.CalculateAsync(modifiedStream, output: patchStream); + await patchSource.CreateAsync(modifiedStream, output: patchStream); // Assert originalStream.Position = 0; diff --git a/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs b/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs index 9eff813..b96b59d 100644 --- a/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs +++ b/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs @@ -26,7 +26,7 @@ public static async ValueTask CreateAsync(Stream original, in return new BinaryPatchSource(blockInfoContainer, blockSize); } - public async ValueTask CalculateAsync(Stream modified, Stream output, CancellationToken cancellationToken = default) + public async ValueTask CreateAsync(Stream modified, Stream output, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(modified); ArgumentNullException.ThrowIfNull(output); From 2d8481bcda3704bbe0299f4362472a97dfdc7426 Mon Sep 17 00:00:00 2001 From: bitc0der <59016822+bitc0der@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:43:20 +0700 Subject: [PATCH 09/20] Add stream checks --- src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs b/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs index b96b59d..e566d29 100644 --- a/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs +++ b/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs @@ -31,6 +31,11 @@ public async ValueTask CreateAsync(Stream modified, Stream output, CancellationT ArgumentNullException.ThrowIfNull(modified); ArgumentNullException.ThrowIfNull(output); + if (!modified.CanRead) + throw new ArgumentException($"{nameof(modified)} does not support reading.", nameof(modified)); + if (!output.CanWrite) + throw new ArgumentException($"{nameof(output)} does not support writing.", nameof(output)); + using var writer = new PatchWriter(output); await writer.WriteHeaderAsync(blockSize: _blockSize, cancellationToken); @@ -174,6 +179,10 @@ public static async ValueTask ApplyAsync( throw new ArgumentException("source stream must be readable.", nameof(source)); if (!source.CanSeek) throw new ArgumentException("source stream must be seekable.", nameof(source)); + if (!patch.CanRead) + throw new ArgumentException("patch stream must be readable.", nameof(patch)); + if (!output.CanWrite) + throw new ArgumentException("output stream must be writable.", nameof(output)); using var reader = new PatchReader(patch); var blockSize = await reader.InitializeAsync(cancellationToken); From 81c0b35439159c0521857ef44ae2a9bd1a28b6ae Mon Sep 17 00:00:00 2001 From: bitc0der <59016822+bitc0der@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:51:24 +0700 Subject: [PATCH 10/20] Rename binary patch source --- .../Patch/BinaryPatchTests.cs | 44 +++++++++---------- .../{BinaryPatchSource.cs => BinaryPatch.cs} | 44 ++++++++----------- 2 files changed, 38 insertions(+), 50 deletions(-) rename src/BitSoft.BinaryTools/Patch/{BinaryPatchSource.cs => BinaryPatch.cs} (86%) diff --git a/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs b/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs index d9067de..a3334f7 100644 --- a/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs +++ b/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs @@ -11,24 +11,23 @@ public class BinaryPatchTests public async Task Should_ReturnBinaryPatchSegment_When_ModifiedSameLength() { // Arrange - var original = new byte[] { 0x0, 0x1, 0x0, 0x1, 0x0 }; + var source = new byte[] { 0x0, 0x1, 0x0, 0x1, 0x0 }; var modified = new byte[] { 0x0, 0x0, 0x1, 0x0, 0x0 }; - using var originalStream = new MemoryStream(original); + using var sourceStream = new MemoryStream(source); using var modifiedStream = new MemoryStream(modified); using var patchStream = new MemoryStream(); // Act - var patchSource = await BinaryPatchSource.CreateAsync(originalStream, blockSize: 2); - await patchSource.CreateAsync(modifiedStream, output: patchStream); + await BinaryPatch.CreateAsync(source: sourceStream, modified: modifiedStream, output: patchStream, blockSize: 2); // Assert - originalStream.Position = 0; + sourceStream.Position = 0; patchStream.Position = 0; using var patchedStream = new MemoryStream(); - await BinaryPatchSource.ApplyAsync(source: originalStream, patch: patchStream, output: patchedStream); + await BinaryPatch.ApplyAsync(source: sourceStream, patch: patchStream, output: patchedStream); var patched = patchedStream.ToArray(); @@ -41,24 +40,23 @@ public async Task Should_ReturnBinaryPatchSegment_When_ModifiedSameLength() public async Task Should_ReturnEndOfFilePatchSegment_When_ModifiedShorterThanOriginal() { // Arrange - var original = new byte[] { 0x0, 0x1 }; + var source = new byte[] { 0x0, 0x1 }; var modified = new byte[] { 0x0 }; - using var originalStream = new MemoryStream(original); + using var sourceStream = new MemoryStream(source); using var modifiedStream = new MemoryStream(modified); using var patchStream = new MemoryStream(); // Act - var patchSource = await BinaryPatchSource.CreateAsync(originalStream, blockSize: 2); - await patchSource.CreateAsync(modifiedStream, output: patchStream); + await BinaryPatch.CreateAsync(source: sourceStream, modified: modifiedStream, output: patchStream, blockSize: 2); // Assert - originalStream.Position = 0; + sourceStream.Position = 0; patchStream.Position = 0; using var patchedStream = new MemoryStream(); - await BinaryPatchSource.ApplyAsync(source: originalStream, patch: patchStream, output: patchedStream); + await BinaryPatch.ApplyAsync(source: sourceStream, patch: patchStream, output: patchedStream); var patched = patchedStream.ToArray(); @@ -71,24 +69,23 @@ public async Task Should_ReturnEndOfFilePatchSegment_When_ModifiedShorterThanOri public async Task Should_ReturnBinaryPatchSegment_When_ModifiedLongerThanOriginal() { // Arrange - var original = new byte[] { 0x0 }; + var source = new byte[] { 0x0 }; var modified = new byte[] { 0x1, 0x2 }; - using var originalStream = new MemoryStream(original); + using var sourceStream = new MemoryStream(source); using var modifiedStream = new MemoryStream(modified); using var patchStream = new MemoryStream(); // Act - var patchSource = await BinaryPatchSource.CreateAsync(originalStream, blockSize: 2); - await patchSource.CreateAsync(modifiedStream, output: patchStream); + await BinaryPatch.CreateAsync(source: sourceStream, modified: modifiedStream, output: patchStream, blockSize: 2); // Assert - originalStream.Position = 0; + sourceStream.Position = 0; patchStream.Position = 0; using var patchedStream = new MemoryStream(); - await BinaryPatchSource.ApplyAsync(source: originalStream, patch: patchStream, output: patchedStream); + await BinaryPatch.ApplyAsync(source: sourceStream, patch: patchStream, output: patchedStream); var patched = patchedStream.ToArray(); @@ -101,24 +98,23 @@ public async Task Should_ReturnBinaryPatchSegment_When_ModifiedLongerThanOrigina public async Task Should_ReturnBinaryPatchSegment_When_ModifiedLongerAndDifferent() { // Arrange - var original = new byte[] { 0x1, 0x2 }; + var source = new byte[] { 0x1, 0x2 }; var modified = new byte[] { 0x3, 0x4, 0x5 }; - using var originalStream = new MemoryStream(original); + using var sourceStream = new MemoryStream(source); using var modifiedStream = new MemoryStream(modified); using var patchStream = new MemoryStream(); // Act - var patchSource = await BinaryPatchSource.CreateAsync(originalStream, blockSize: 2); - await patchSource.CreateAsync(modifiedStream, output: patchStream); + await BinaryPatch.CreateAsync(source: sourceStream, modified: modifiedStream, output: patchStream, blockSize: 2); // Assert - originalStream.Position = 0; + sourceStream.Position = 0; patchStream.Position = 0; using var patchedStream = new MemoryStream(); - await BinaryPatchSource.ApplyAsync(source: originalStream, patch: patchStream, output: patchedStream); + await BinaryPatch.ApplyAsync(source: sourceStream, patch: patchStream, output: patchedStream); var patched = patchedStream.ToArray(); diff --git a/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs b/src/BitSoft.BinaryTools/Patch/BinaryPatch.cs similarity index 86% rename from src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs rename to src/BitSoft.BinaryTools/Patch/BinaryPatch.cs index e566d29..4c0c8bb 100644 --- a/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs +++ b/src/BitSoft.BinaryTools/Patch/BinaryPatch.cs @@ -6,28 +6,18 @@ namespace BitSoft.BinaryTools.Patch; -public sealed class BinaryPatchSource +public sealed class BinaryPatch { - private readonly BlockInfoContainer _blockInfoContainer; - private readonly int _blockSize; - private static readonly ArrayPool Pool = ArrayPool.Shared; - private BinaryPatchSource(BlockInfoContainer blockInfoContainer, int blockSize) - { - _blockInfoContainer = blockInfoContainer ?? throw new ArgumentNullException(nameof(blockInfoContainer)); - _blockSize = blockSize; - } - - public static async ValueTask CreateAsync(Stream original, int blockSize = 4 * 1024) - { - ArgumentNullException.ThrowIfNull(original); - var blockInfoContainer = await CalculateHashesAsync(original, blockSize); - return new BinaryPatchSource(blockInfoContainer, blockSize); - } - - public async ValueTask CreateAsync(Stream modified, Stream output, CancellationToken cancellationToken = default) + public static async ValueTask CreateAsync( + Stream source, + Stream modified, + Stream output, + int blockSize = 4 * 1024, + CancellationToken cancellationToken = default) { + ArgumentNullException.ThrowIfNull(source); ArgumentNullException.ThrowIfNull(modified); ArgumentNullException.ThrowIfNull(output); @@ -36,11 +26,13 @@ public async ValueTask CreateAsync(Stream modified, Stream output, CancellationT if (!output.CanWrite) throw new ArgumentException($"{nameof(output)} does not support writing.", nameof(output)); + var blockInfoContainer = await CalculateHashesAsync(source, blockSize, cancellationToken); + using var writer = new PatchWriter(output); - await writer.WriteHeaderAsync(blockSize: _blockSize, cancellationToken); + await writer.WriteHeaderAsync(blockSize: blockSize, cancellationToken); - var bufferLength = _blockSize * 2; + var bufferLength = blockSize * 2; var buffer = Pool.Rent(minimumLength: bufferLength); try { @@ -62,17 +54,17 @@ public async ValueTask CreateAsync(Stream modified, Stream output, CancellationT { if (resetHash) { - var spanLength = Math.Min(_blockSize, length); + var spanLength = Math.Min(blockSize, length); var bufferSpan = buffer.AsSpan(start: 0, length: spanLength); rollingHash = RollingHash.Create(bufferSpan); resetHash = false; } - var block = _blockInfoContainer.Match(rollingHash); + var block = blockInfoContainer.Match(rollingHash); if (block is null) { - if (length <= _blockSize) + if (length <= blockSize) { var memory = buffer.AsMemory(start: position, length: length); await writer.WriteDataAsync(memory, cancellationToken); @@ -84,7 +76,7 @@ public async ValueTask CreateAsync(Stream modified, Stream output, CancellationT { segmentStart = position; } - else if (position - segmentStart + 1 == _blockSize) + else if (position - segmentStart + 1 == blockSize) { var memory = buffer.AsMemory(start: segmentStart, length: position - segmentStart + 1); await writer.WriteDataAsync(memory, cancellationToken); @@ -109,10 +101,10 @@ public async ValueTask CreateAsync(Stream modified, Stream output, CancellationT break; } - if (position + _blockSize < length) + if (position + blockSize < length) { var removedByte = buffer[position - 1]; - var addedByte = buffer[position + _blockSize - 1]; + var addedByte = buffer[position + blockSize - 1]; rollingHash.Update(removed: removedByte, added: addedByte); } else From 7c07a9ff2de8036e4179d7ee255bc1c388e78382 Mon Sep 17 00:00:00 2001 From: bitc0der <59016822+bitc0der@users.noreply.github.com> Date: Tue, 25 Nov 2025 19:25:23 +0700 Subject: [PATCH 11/20] Simplify tests --- .../Patch/BinaryPatchTests.cs | 120 +++++------------- 1 file changed, 29 insertions(+), 91 deletions(-) diff --git a/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs b/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs index a3334f7..d59e315 100644 --- a/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs +++ b/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using BitSoft.BinaryTools.Patch; @@ -7,106 +8,43 @@ namespace BitSoft.BinaryTools.Tests.Patch; [TestFixture] public class BinaryPatchTests { - [Test] - public async Task Should_ReturnBinaryPatchSegment_When_ModifiedSameLength() + private static IEnumerable TestCases() { - // Arrange - var source = new byte[] { 0x0, 0x1, 0x0, 0x1, 0x0 }; - var modified = new byte[] { 0x0, 0x0, 0x1, 0x0, 0x0 }; - - using var sourceStream = new MemoryStream(source); - using var modifiedStream = new MemoryStream(modified); - using var patchStream = new MemoryStream(); - - // Act - await BinaryPatch.CreateAsync(source: sourceStream, modified: modifiedStream, output: patchStream, blockSize: 2); - - // Assert - sourceStream.Position = 0; - patchStream.Position = 0; - - using var patchedStream = new MemoryStream(); - - await BinaryPatch.ApplyAsync(source: sourceStream, patch: patchStream, output: patchedStream); - - var patched = patchedStream.ToArray(); - - Assert.That(patched, Is.Not.Null); - Assert.That(patched.Length, Is.EqualTo(modified.Length)); - Assert.That(patched, Is.EqualTo(modified)); + yield return new TestCaseData( + new byte[] { 0x0, 0x1, 0x0, 0x1, 0x0 }, + new byte[] { 0x0, 0x0, 0x1, 0x0, 0x0 }, + 2 + ); + yield return new TestCaseData( + new byte[] { 0x0, 0x1 }, + new byte[] { 0x0 }, + 2 + ); + yield return new TestCaseData( + new byte[] { 0x0 }, + new byte[] { 0x1, 0x2 }, + 2 + ); + yield return new TestCaseData( + new byte[] { 0x1, 0x2 }, + new byte[] { 0x3, 0x4, 0x5 }, + 2 + ); } - [Test] - public async Task Should_ReturnEndOfFilePatchSegment_When_ModifiedShorterThanOriginal() + [TestCaseSource(nameof(TestCases))] + public async Task Should_CreatPatch(byte[] source, byte[] modified, int blockSize) { - // Arrange - var source = new byte[] { 0x0, 0x1 }; - var modified = new byte[] { 0x0 }; - - using var sourceStream = new MemoryStream(source); - using var modifiedStream = new MemoryStream(modified); - using var patchStream = new MemoryStream(); - - // Act - await BinaryPatch.CreateAsync(source: sourceStream, modified: modifiedStream, output: patchStream, blockSize: 2); - - // Assert - sourceStream.Position = 0; - patchStream.Position = 0; - - using var patchedStream = new MemoryStream(); - - await BinaryPatch.ApplyAsync(source: sourceStream, patch: patchStream, output: patchedStream); - - var patched = patchedStream.ToArray(); - - Assert.That(patched, Is.Not.Null); - Assert.That(patched.Length, Is.EqualTo(modified.Length)); - Assert.That(patched, Is.EqualTo(modified)); - } - - [Test] - public async Task Should_ReturnBinaryPatchSegment_When_ModifiedLongerThanOriginal() - { - // Arrange - var source = new byte[] { 0x0 }; - var modified = new byte[] { 0x1, 0x2 }; - - using var sourceStream = new MemoryStream(source); - using var modifiedStream = new MemoryStream(modified); - using var patchStream = new MemoryStream(); - - // Act - await BinaryPatch.CreateAsync(source: sourceStream, modified: modifiedStream, output: patchStream, blockSize: 2); - - // Assert - sourceStream.Position = 0; - patchStream.Position = 0; - - using var patchedStream = new MemoryStream(); - - await BinaryPatch.ApplyAsync(source: sourceStream, patch: patchStream, output: patchedStream); - - var patched = patchedStream.ToArray(); - - Assert.That(patched, Is.Not.Null); - Assert.That(patched.Length, Is.EqualTo(modified.Length)); - Assert.That(patched, Is.EqualTo(modified)); - } - - [Test] - public async Task Should_ReturnBinaryPatchSegment_When_ModifiedLongerAndDifferent() - { - // Arrange - var source = new byte[] { 0x1, 0x2 }; - var modified = new byte[] { 0x3, 0x4, 0x5 }; - using var sourceStream = new MemoryStream(source); using var modifiedStream = new MemoryStream(modified); using var patchStream = new MemoryStream(); // Act - await BinaryPatch.CreateAsync(source: sourceStream, modified: modifiedStream, output: patchStream, blockSize: 2); + await BinaryPatch.CreateAsync( + source: sourceStream, + modified: modifiedStream, + output: patchStream, + blockSize: blockSize); // Assert sourceStream.Position = 0; From f7f18b8a53802106136b73865a008991fec3b7f9 Mon Sep 17 00:00:00 2001 From: bitc0der <59016822+bitc0der@users.noreply.github.com> Date: Tue, 25 Nov 2025 19:28:43 +0700 Subject: [PATCH 12/20] Add editorconfig --- .editorconfig | 153 ++++++++++++++++++++++++++++++++++++ src/BitSoft.BinaryTools.sln | 1 + 2 files changed, 154 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b87a9b6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,153 @@ + +[*.cs] + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.private_or_internal_field_should_be_camelwith_.severity = suggestion +dotnet_naming_rule.private_or_internal_field_should_be_camelwith_.symbols = private_or_internal_field +dotnet_naming_rule.private_or_internal_field_should_be_camelwith_.style = camelwith_ + +# Symbol specifications + +dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field +dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected +dotnet_naming_symbols.private_or_internal_field.required_modifiers = + +# Naming styles + +dotnet_naming_style.camelwith_.required_prefix = _ +dotnet_naming_style.camelwith_.required_suffix = +dotnet_naming_style.camelwith_.word_separator = +dotnet_naming_style.camelwith_.capitalization = camel_case +csharp_indent_labels = one_less_than_current +csharp_using_directive_placement = outside_namespace:error +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_namespace_declarations = file_scoped:warning +csharp_style_prefer_method_group_conversion = true:warning +csharp_style_prefer_top_level_statements = true:warning +csharp_style_prefer_primary_constructors = true:suggestion +csharp_style_expression_bodied_methods = when_on_single_line:suggestion +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = when_on_single_line:suggestion +csharp_style_expression_bodied_properties = when_on_single_line:suggestion +csharp_style_expression_bodied_indexers = when_on_single_line:suggestion +csharp_style_expression_bodied_accessors = when_on_single_line:suggestion +csharp_style_expression_bodied_lambdas = when_on_single_line:suggestion +csharp_style_expression_bodied_local_functions = when_on_single_line:suggestion +csharp_space_around_binary_operators = before_and_after +csharp_style_throw_expression = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:error +csharp_style_unused_value_expression_statement_preference = discard_variable:silent +csharp_prefer_static_local_function = true:suggestion +csharp_style_prefer_readonly_struct = true:suggestion +csharp_style_prefer_readonly_struct_member = true:suggestion +csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:suggestion +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent +csharp_style_conditional_delegate_call = true:suggestion +csharp_style_prefer_switch_expression = true:suggestion +csharp_style_prefer_pattern_matching = true:silent +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion +csharp_style_var_for_built_in_types = true:warning +csharp_style_var_when_type_is_apparent = true:warning +csharp_style_var_elsewhere = true:warning + +[*.{cs,vb}] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:suggestion +dotnet_style_explicit_tuple_names = true:error +dotnet_style_prefer_inferred_tuple_names = true:error +dotnet_style_prefer_inferred_anonymous_type_member_names = true:error +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:warning +dotnet_style_namespace_match_folder = true:suggestion +dotnet_style_readonly_field = true:warning +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent +dotnet_style_allow_multiple_blank_lines_experimental = false:warning +dotnet_style_allow_statement_immediately_after_block_experimental = true:warning +dotnet_code_quality_unused_parameters = all:suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_event = false:warning diff --git a/src/BitSoft.BinaryTools.sln b/src/BitSoft.BinaryTools.sln index 70e6a03..90fe38e 100644 --- a/src/BitSoft.BinaryTools.sln +++ b/src/BitSoft.BinaryTools.sln @@ -7,6 +7,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution items", "Solution ..\.gitignore = ..\.gitignore ..\Directory.Build.props = ..\Directory.Build.props ..\Directory.Packages.props = ..\Directory.Packages.props + ..\.editorconfig = ..\.editorconfig EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BitSoft.BinaryTools", "BitSoft.BinaryTools\BitSoft.BinaryTools.csproj", "{19535389-2AAA-42AD-84E2-41905D196A5A}" From b34094e9f88fc6cd5f30efc7d4aff7a3230f4acc Mon Sep 17 00:00:00 2001 From: bitc0der <59016822+bitc0der@users.noreply.github.com> Date: Tue, 25 Nov 2025 19:31:28 +0700 Subject: [PATCH 13/20] Set patch static --- src/BitSoft.BinaryTools/Patch/BinaryPatch.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BitSoft.BinaryTools/Patch/BinaryPatch.cs b/src/BitSoft.BinaryTools/Patch/BinaryPatch.cs index 4c0c8bb..c2bdd39 100644 --- a/src/BitSoft.BinaryTools/Patch/BinaryPatch.cs +++ b/src/BitSoft.BinaryTools/Patch/BinaryPatch.cs @@ -6,7 +6,7 @@ namespace BitSoft.BinaryTools.Patch; -public sealed class BinaryPatch +public static class BinaryPatch { private static readonly ArrayPool Pool = ArrayPool.Shared; From 632d6c5c371cd38e97134bff6dd85893c2c1259e Mon Sep 17 00:00:00 2001 From: bitc0der <59016822+bitc0der@users.noreply.github.com> Date: Tue, 25 Nov 2025 19:57:17 +0700 Subject: [PATCH 14/20] Arrange style --- .editorconfig | 323 ++++++++++-------- src/BitSoft.BinaryTools/Patch/BinaryPatch.cs | 11 +- .../Patch/CopyPatchSegment.cs | 8 + .../Patch/DataPatchSegment.cs | 8 + .../Patch/IPatchSegment.cs | 5 + .../Patch/PatchBlockInfo.cs | 17 +- src/BitSoft.BinaryTools/Patch/PatchReader.cs | 35 +- src/BitSoft.BinaryTools/Patch/PatchWriter.cs | 9 +- 8 files changed, 222 insertions(+), 194 deletions(-) create mode 100644 src/BitSoft.BinaryTools/Patch/CopyPatchSegment.cs create mode 100644 src/BitSoft.BinaryTools/Patch/DataPatchSegment.cs create mode 100644 src/BitSoft.BinaryTools/Patch/IPatchSegment.cs diff --git a/.editorconfig b/.editorconfig index b87a9b6..4469c5b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,153 +1,196 @@ +# editorconfig.org -[*.cs] - -#### Naming styles #### - -# Naming rules +# top-most EditorConfig file +root = true -dotnet_naming_rule.private_or_internal_field_should_be_camelwith_.severity = suggestion -dotnet_naming_rule.private_or_internal_field_should_be_camelwith_.symbols = private_or_internal_field -dotnet_naming_rule.private_or_internal_field_should_be_camelwith_.style = camelwith_ - -# Symbol specifications +# Default settings: +# A newline ending every file +# Use 4 spaces as indentation +[*] +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true -dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field -dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected -dotnet_naming_symbols.private_or_internal_field.required_modifiers = +# Specify UTF-8 without byte-order mark +[*.{csproj,locproj,nativeproj,proj,resx,slnx,vbproj}] +charset = utf-8 -# Naming styles +# Generated code +[*{_AssemblyInfo.cs,.notsupported.cs,AsmOffsets.cs}] +generated_code = true -dotnet_naming_style.camelwith_.required_prefix = _ -dotnet_naming_style.camelwith_.required_suffix = -dotnet_naming_style.camelwith_.word_separator = -dotnet_naming_style.camelwith_.capitalization = camel_case +# C# files +[*.cs] +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = false +csharp_indent_switch_labels = true csharp_indent_labels = one_less_than_current -csharp_using_directive_placement = outside_namespace:error -csharp_prefer_simple_using_statement = true:suggestion + +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion + +# avoid this. unless absolutely necessary +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +# Types: use keywords instead of BCL types, and permit var only when the type is clear +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = false:none +csharp_style_var_elsewhere = false:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# name all constant fields using PascalCase +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.required_modifiers = const +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# static fields should have s_ prefix +dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion +dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields +dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static +dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected +dotnet_naming_style.static_prefix_style.capitalization = pascal_case + +# internal and private fields should be _camelCase +dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion +dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields +dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style +dotnet_naming_symbols.private_internal_fields.applicable_kinds = field +dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal +dotnet_naming_style.camel_case_underscore_style.required_prefix = _ +dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case + +# Code style defaults +csharp_using_directive_placement = outside_namespace:suggestion +dotnet_sort_system_directives_first = true csharp_prefer_braces = true:silent -csharp_style_namespace_declarations = file_scoped:warning -csharp_style_prefer_method_group_conversion = true:warning -csharp_style_prefer_top_level_statements = true:warning -csharp_style_prefer_primary_constructors = true:suggestion -csharp_style_expression_bodied_methods = when_on_single_line:suggestion -csharp_style_expression_bodied_constructors = false:silent -csharp_style_expression_bodied_operators = when_on_single_line:suggestion -csharp_style_expression_bodied_properties = when_on_single_line:suggestion -csharp_style_expression_bodied_indexers = when_on_single_line:suggestion -csharp_style_expression_bodied_accessors = when_on_single_line:suggestion -csharp_style_expression_bodied_lambdas = when_on_single_line:suggestion -csharp_style_expression_bodied_local_functions = when_on_single_line:suggestion -csharp_space_around_binary_operators = before_and_after -csharp_style_throw_expression = true:suggestion -csharp_style_prefer_null_check_over_type_check = true:suggestion -csharp_prefer_simple_default_expression = true:suggestion -csharp_style_prefer_local_over_anonymous_function = true:suggestion -csharp_style_prefer_index_operator = true:suggestion -csharp_style_prefer_range_operator = true:suggestion -csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion -csharp_style_prefer_tuple_swap = true:suggestion -csharp_style_prefer_utf8_string_literals = true:suggestion -csharp_style_inlined_variable_declaration = true:suggestion -csharp_style_deconstructed_variable_declaration = true:suggestion -csharp_style_unused_value_assignment_preference = discard_variable:error -csharp_style_unused_value_expression_statement_preference = discard_variable:silent +csharp_preserve_single_line_blocks = true:none +csharp_preserve_single_line_statements = false:none csharp_prefer_static_local_function = true:suggestion -csharp_style_prefer_readonly_struct = true:suggestion -csharp_style_prefer_readonly_struct_member = true:suggestion -csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent -csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:suggestion -csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent -csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent -csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent -csharp_style_conditional_delegate_call = true:suggestion +csharp_prefer_simple_using_statement = false:none csharp_style_prefer_switch_expression = true:suggestion -csharp_style_prefer_pattern_matching = true:silent -csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion -csharp_style_pattern_matching_over_as_with_null_check = true:suggestion -csharp_style_prefer_not_pattern = true:suggestion -csharp_style_prefer_extended_property_pattern = true:suggestion -csharp_style_var_for_built_in_types = true:warning -csharp_style_var_when_type_is_apparent = true:warning -csharp_style_var_elsewhere = true:warning - -[*.{cs,vb}] -#### Naming styles #### - -# Naming rules - -dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion -dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface -dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i - -dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.types_should_be_pascal_case.symbols = types -dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case - -dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members -dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case - -# Symbol specifications +dotnet_style_readonly_field = true:suggestion -dotnet_naming_symbols.interface.applicable_kinds = interface -dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.interface.required_modifiers = - -dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum -dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.types.required_modifiers = - -dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method -dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.non_field_members.required_modifiers = - -# Naming styles +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_collection_expression = when_types_exactly_match +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +csharp_prefer_simple_default_expression = true:suggestion -dotnet_naming_style.begins_with_i.required_prefix = I -dotnet_naming_style.begins_with_i.required_suffix = -dotnet_naming_style.begins_with_i.word_separator = -dotnet_naming_style.begins_with_i.capitalization = pascal_case +# Expression-bodied members +csharp_style_expression_bodied_methods = true:silent +csharp_style_expression_bodied_constructors = true:silent +csharp_style_expression_bodied_operators = true:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = true:silent + +# Pattern matching +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = -dotnet_naming_style.pascal_case.capitalization = pascal_case +# Null checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = -dotnet_naming_style.pascal_case.capitalization = pascal_case -dotnet_style_operator_placement_when_wrapping = beginning_of_line -tab_width = 4 -indent_size = 4 -end_of_line = crlf -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_null_propagation = true:suggestion -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion -dotnet_style_prefer_auto_properties = true:silent -dotnet_style_object_initializer = true:suggestion -dotnet_style_collection_initializer = true:suggestion -dotnet_style_prefer_simplified_boolean_expressions = true:suggestion -dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion -dotnet_style_prefer_conditional_expression_over_return = true:suggestion -dotnet_style_explicit_tuple_names = true:error -dotnet_style_prefer_inferred_tuple_names = true:error -dotnet_style_prefer_inferred_anonymous_type_member_names = true:error -dotnet_style_prefer_compound_assignment = true:suggestion -dotnet_style_prefer_simplified_interpolation = true:warning -dotnet_style_namespace_match_folder = true:suggestion -dotnet_style_readonly_field = true:warning -dotnet_style_predefined_type_for_locals_parameters_members = true:silent -dotnet_style_predefined_type_for_member_access = true:silent -dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent -dotnet_style_allow_multiple_blank_lines_experimental = false:warning -dotnet_style_allow_statement_immediately_after_block_experimental = true:warning -dotnet_code_quality_unused_parameters = all:suggestion -dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent -dotnet_style_qualification_for_field = false:warning -dotnet_style_qualification_for_property = false:warning -dotnet_style_qualification_for_method = false:warning -dotnet_style_qualification_for_event = false:warning +# Other features +csharp_style_prefer_index_operator = false:none +csharp_style_prefer_range_operator = false:none +csharp_style_pattern_local_over_anonymous_function = false:none + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = do_not_ignore +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# disable CA2025, the analyzer throws a NullReferenceException when processing this file: https://github.com/dotnet/roslyn-analyzers/issues/7652 +dotnet_diagnostic.CA2025.severity = none + +# C++ Files +[*.{cpp,h,in}] +curly_bracket_next_line = true +indent_brace_style = Allman + +# Xml project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] +indent_size = 2 + +# Xml build files +[*.builds] +indent_size = 2 + +# Xml files +[*.{resx,ruleset,slnx,stylecop,xml}] +indent_size = 2 + +# Xml resource files +[*.resx] +# match Visual Studio behavior +insert_final_newline = false +trim_trailing_whitespace = false + +# Xml config files +[*.{props,targets,config,nuspec}] +indent_size = 2 + +# Data serialization +[*.{json,yaml,yml}] +indent_size = 2 + +# Shell scripts +[*.sh] +end_of_line = lf +[*.{cmd,bat}] +end_of_line = crlf \ No newline at end of file diff --git a/src/BitSoft.BinaryTools/Patch/BinaryPatch.cs b/src/BitSoft.BinaryTools/Patch/BinaryPatch.cs index c2bdd39..3bbd568 100644 --- a/src/BitSoft.BinaryTools/Patch/BinaryPatch.cs +++ b/src/BitSoft.BinaryTools/Patch/BinaryPatch.cs @@ -192,11 +192,10 @@ public static async ValueTask ApplyAsync( var buffer = Pool.Rent(copyPatchSegment.BlockLength); try { - var count = await source.ReadAsync(buffer, - offset: 0, - count: copyPatchSegment.BlockLength, - cancellationToken); - await output.WriteAsync(buffer, offset: 0, count: count, cancellationToken); + var memory = buffer.AsMemory(start: 0, length: copyPatchSegment.BlockLength); + var count = await source.ReadAsync(memory, cancellationToken); + if (count != copyPatchSegment.BlockLength) throw new InvalidOperationException(); + await output.WriteAsync(memory, cancellationToken); } finally { @@ -250,4 +249,4 @@ private static async ValueTask CalculateHashesAsync( return blockInfoContainer; } -} \ No newline at end of file +} diff --git a/src/BitSoft.BinaryTools/Patch/CopyPatchSegment.cs b/src/BitSoft.BinaryTools/Patch/CopyPatchSegment.cs new file mode 100644 index 0000000..c42a4ff --- /dev/null +++ b/src/BitSoft.BinaryTools/Patch/CopyPatchSegment.cs @@ -0,0 +1,8 @@ +namespace BitSoft.BinaryTools.Patch; + +internal sealed class CopyPatchSegment(int blockIndex, int blockLength) : IPatchSegment +{ + public int BlockIndex { get; } = blockIndex; + + public int BlockLength { get; } = blockLength; +} diff --git a/src/BitSoft.BinaryTools/Patch/DataPatchSegment.cs b/src/BitSoft.BinaryTools/Patch/DataPatchSegment.cs new file mode 100644 index 0000000..c84dc1e --- /dev/null +++ b/src/BitSoft.BinaryTools/Patch/DataPatchSegment.cs @@ -0,0 +1,8 @@ +using System; + +namespace BitSoft.BinaryTools.Patch; + +internal sealed class DataPatchSegment(ReadOnlyMemory data) : IPatchSegment +{ + public ReadOnlyMemory Data { get; } = data; +} diff --git a/src/BitSoft.BinaryTools/Patch/IPatchSegment.cs b/src/BitSoft.BinaryTools/Patch/IPatchSegment.cs new file mode 100644 index 0000000..7f9824c --- /dev/null +++ b/src/BitSoft.BinaryTools/Patch/IPatchSegment.cs @@ -0,0 +1,5 @@ +namespace BitSoft.BinaryTools.Patch; + +internal interface IPatchSegment +{ +} diff --git a/src/BitSoft.BinaryTools/Patch/PatchBlockInfo.cs b/src/BitSoft.BinaryTools/Patch/PatchBlockInfo.cs index 62a990b..92d5786 100644 --- a/src/BitSoft.BinaryTools/Patch/PatchBlockInfo.cs +++ b/src/BitSoft.BinaryTools/Patch/PatchBlockInfo.cs @@ -1,17 +1,10 @@ namespace BitSoft.BinaryTools.Patch; -public sealed class PatchBlockInfo +public sealed class PatchBlockInfo(int blockIndex, uint hash, int length) { - public int BlockIndex { get; } + public int BlockIndex { get; } = blockIndex; - public uint Hash { get; } + public uint Hash { get; } = hash; - public int Length { get; } - - public PatchBlockInfo(int blockIndex, uint hash, int length) - { - BlockIndex = blockIndex; - Hash = hash; - Length = length; - } -} \ No newline at end of file + public int Length { get; } = length; +} diff --git a/src/BitSoft.BinaryTools/Patch/PatchReader.cs b/src/BitSoft.BinaryTools/Patch/PatchReader.cs index bc2baf1..5e22ac9 100644 --- a/src/BitSoft.BinaryTools/Patch/PatchReader.cs +++ b/src/BitSoft.BinaryTools/Patch/PatchReader.cs @@ -6,46 +6,19 @@ namespace BitSoft.BinaryTools.Patch; -internal interface IPatchSegment -{ -} - -internal sealed class DataPatchSegment : IPatchSegment -{ - public DataPatchSegment(ReadOnlyMemory data) - { - Data = data; - } - - public ReadOnlyMemory Data { get; } -} - -internal sealed class CopyPatchSegment : IPatchSegment -{ - public int BlockIndex { get; } - - public int BlockLength { get; } - - public CopyPatchSegment(int blockIndex, int blockLength) - { - BlockIndex = blockIndex; - BlockLength = blockLength; - } -} - internal sealed class PatchReader : IDisposable { - private readonly Stream _source; private readonly BinaryReader _reader; private static readonly ArrayPool Pool = ArrayPool.Shared; private byte[]? _buffer; - public IPatchSegment? Segment { get; private set; } = null; + public IPatchSegment? Segment { get; private set; } public PatchReader(Stream source) { - _source = source ?? throw new ArgumentNullException(nameof(source)); + ArgumentNullException.ThrowIfNull(source); + _reader = new BinaryReader(source, encoding: ProtocolConst.DefaultEncoding, leaveOpen: true); } @@ -97,4 +70,4 @@ public void Dispose() if (_buffer is not null) Pool.Return(_buffer); } -} \ No newline at end of file +} diff --git a/src/BitSoft.BinaryTools/Patch/PatchWriter.cs b/src/BitSoft.BinaryTools/Patch/PatchWriter.cs index 913dbd3..723cbdb 100644 --- a/src/BitSoft.BinaryTools/Patch/PatchWriter.cs +++ b/src/BitSoft.BinaryTools/Patch/PatchWriter.cs @@ -7,14 +7,13 @@ namespace BitSoft.BinaryTools.Patch; internal sealed class PatchWriter : IDisposable { - private readonly Stream _output; - private readonly BinaryWriter _writer; public PatchWriter(Stream output) { - _output = output ?? throw new ArgumentNullException(nameof(output)); - _writer = new BinaryWriter(_output, encoding: ProtocolConst.DefaultEncoding, leaveOpen: true); + ArgumentNullException.ThrowIfNull(output); + + _writer = new BinaryWriter(output, encoding: ProtocolConst.DefaultEncoding, leaveOpen: true); } public ValueTask WriteHeaderAsync(int blockSize, CancellationToken cancellationToken) @@ -53,4 +52,4 @@ public void Dispose() { _writer.Dispose(); } -} \ No newline at end of file +} From 823208f53215edc50e88f8842c5d5ebfb3755f0c Mon Sep 17 00:00:00 2001 From: bitc0der <59016822+bitc0der@users.noreply.github.com> Date: Tue, 25 Nov 2025 19:57:42 +0700 Subject: [PATCH 15/20] Add reference source --- .editorconfig | 1 + 1 file changed, 1 insertion(+) diff --git a/.editorconfig b/.editorconfig index 4469c5b..7accc10 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,5 @@ # editorconfig.org +# based on: https://github.com/dotnet/runtime/blob/main/.editorconfig # top-most EditorConfig file root = true From 5247c34d13d84e2e6c57ac340c469ba26b8bb916 Mon Sep 17 00:00:00 2001 From: bitc0der <59016822+bitc0der@users.noreply.github.com> Date: Tue, 25 Nov 2025 19:59:01 +0700 Subject: [PATCH 16/20] Bump refs --- Directory.Packages.props | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 4c20460..4c05ccc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,9 +1,9 @@  - - - - - - - + + + + + + + \ No newline at end of file From 51fc882a16f3f6cf2599779248905691fb22ab77 Mon Sep 17 00:00:00 2001 From: bitc0der <59016822+bitc0der@users.noreply.github.com> Date: Tue, 25 Nov 2025 20:04:35 +0700 Subject: [PATCH 17/20] Add binary patch --- README.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d56ba00..0794467 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,23 @@ # BitSoft.BinaryTools -Yet another one tools lib for operations with binary data. \ No newline at end of file +Yet another one tools lib for operations with binary data. + +## Binary patch + +Calculate diff and create patch for two streams: + +```csharp +public async ValueTask CreatePatch(Stream source, Stream target, Stream output, CancellationToken token) +{ + await BinaryPatch.CreateAsync(source, target, output, cancellationToken: token); +} +``` + +Apply patch to a source stream: + +```csharp +public async ValueTask ApplyAsync(Stream source, Stream patch, Stream output, CancellationToken token) +{ + await BinaryPatch.ApplyAsync(source, patch, output, cancellationToken: token); +} +``` From 891aae5e6da7f295fa260b1f40494ac690dfbaf2 Mon Sep 17 00:00:00 2001 From: bitc0der <59016822+bitc0der@users.noreply.github.com> Date: Tue, 25 Nov 2025 20:05:52 +0700 Subject: [PATCH 18/20] Add usings --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 0794467..f6a3368 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,10 @@ Yet another one tools lib for operations with binary data. Calculate diff and create patch for two streams: ```csharp +using System.IO; +using System.Threading.Tasks; +using BitSoft.BinaryTools.Patch; + public async ValueTask CreatePatch(Stream source, Stream target, Stream output, CancellationToken token) { await BinaryPatch.CreateAsync(source, target, output, cancellationToken: token); @@ -16,6 +20,10 @@ public async ValueTask CreatePatch(Stream source, Stream target, Stream output, Apply patch to a source stream: ```csharp +using System.IO; +using System.Threading.Tasks; +using BitSoft.BinaryTools.Patch; + public async ValueTask ApplyAsync(Stream source, Stream patch, Stream output, CancellationToken token) { await BinaryPatch.ApplyAsync(source, patch, output, cancellationToken: token); From 970efbcc04c9b7deb4ac83e2e6ef9767b93ddc12 Mon Sep 17 00:00:00 2001 From: bitc0der <59016822+bitc0der@users.noreply.github.com> Date: Tue, 25 Nov 2025 20:06:27 +0700 Subject: [PATCH 19/20] Add postfix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f6a3368..f230f0f 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ using System.IO; using System.Threading.Tasks; using BitSoft.BinaryTools.Patch; -public async ValueTask CreatePatch(Stream source, Stream target, Stream output, CancellationToken token) +public async ValueTask CreatePatchAsync(Stream source, Stream target, Stream output, CancellationToken token) { await BinaryPatch.CreateAsync(source, target, output, cancellationToken: token); } From 18c33addbcbee2d6b359107a77a8df87b9b86982 Mon Sep 17 00:00:00 2001 From: bitc0der <59016822+bitc0der@users.noreply.github.com> Date: Tue, 25 Nov 2025 20:06:55 +0700 Subject: [PATCH 20/20] Add patch to the method name --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f230f0f..0228c6d 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ using System.IO; using System.Threading.Tasks; using BitSoft.BinaryTools.Patch; -public async ValueTask ApplyAsync(Stream source, Stream patch, Stream output, CancellationToken token) +public async ValueTask ApplyPatchAsync(Stream source, Stream patch, Stream output, CancellationToken token) { await BinaryPatch.ApplyAsync(source, patch, output, cancellationToken: token); }