diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..7accc10
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,197 @@
+# editorconfig.org
+# based on: https://github.com/dotnet/runtime/blob/main/.editorconfig
+
+# top-most EditorConfig file
+root = true
+
+# 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
+
+# Specify UTF-8 without byte-order mark
+[*.{csproj,locproj,nativeproj,proj,resx,slnx,vbproj}]
+charset = utf-8
+
+# Generated code
+[*{_AssemblyInfo.cs,.notsupported.cs,AsmOffsets.cs}]
+generated_code = true
+
+# 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
+
+# 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_preserve_single_line_blocks = true:none
+csharp_preserve_single_line_statements = false:none
+csharp_prefer_static_local_function = true:suggestion
+csharp_prefer_simple_using_statement = false:none
+csharp_style_prefer_switch_expression = true:suggestion
+dotnet_style_readonly_field = true:suggestion
+
+# 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
+
+# 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
+
+# Null checking preferences
+csharp_style_throw_expression = true:suggestion
+csharp_style_conditional_delegate_call = true:suggestion
+
+# 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/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
diff --git a/README.md b/README.md
index d56ba00..0228c6d 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,31 @@
# 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
+using System.IO;
+using System.Threading.Tasks;
+using BitSoft.BinaryTools.Patch;
+
+public async ValueTask CreatePatchAsync(Stream source, Stream target, Stream output, CancellationToken token)
+{
+ await BinaryPatch.CreateAsync(source, target, output, cancellationToken: token);
+}
+```
+
+Apply patch to a source stream:
+
+```csharp
+using System.IO;
+using System.Threading.Tasks;
+using BitSoft.BinaryTools.Patch;
+
+public async ValueTask ApplyPatchAsync(Stream source, Stream patch, Stream output, CancellationToken token)
+{
+ await BinaryPatch.ApplyAsync(source, patch, output, cancellationToken: token);
+}
+```
diff --git a/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs b/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs
index cb4cee0..d59e315 100644
--- a/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs
+++ b/src/BitSoft.BinaryTools.Tests/Patch/BinaryPatchTests.cs
@@ -1,4 +1,6 @@
+using System.Collections.Generic;
using System.IO;
+using System.Threading.Tasks;
using BitSoft.BinaryTools.Patch;
namespace BitSoft.BinaryTools.Tests.Patch;
@@ -6,155 +8,56 @@ namespace BitSoft.BinaryTools.Tests.Patch;
[TestFixture]
public class BinaryPatchTests
{
- [Test]
- public void Should_DeserializePatch_FromStream()
+ private static IEnumerable TestCases()
{
- // 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));
+ 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 void Should_ReturnBinaryPatchSegment_When_ModifiedSameLength()
+ [TestCaseSource(nameof(TestCases))]
+ public async Task Should_CreatPatch(byte[] source, byte[] modified, int blockSize)
{
- // Arrange
- var original = 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
- var patchSource = BinaryPatchSource.Create(original, blockSize: 2);
- var patch = patchSource.Calculate(modified);
+ await BinaryPatch.CreateAsync(
+ source: sourceStream,
+ modified: modifiedStream,
+ output: patchStream,
+ blockSize: blockSize);
// 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));
+ sourceStream.Position = 0;
+ patchStream.Position = 0;
using var patchedStream = new MemoryStream();
- BinaryPatchSource.Apply(original, patch, patchedStream);
-
+ 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 void Should_ReturnEndOfFilePatchSegment_When_ModifiedShorterThanOriginal()
- {
- // Arrange
- var original = new byte[] { 0x0, 0x1 };
- var modified = new byte[] { 0x0 };
-
- // Act
- var patchSource = BinaryPatchSource.Create(original, blockSize: 2);
-
- var patch = patchSource.Calculate(modified);
-
- // Assert
- Assert.That(patch, Is.Not.Null);
- Assert.That(patch.Segments, Is.Not.Empty);
- Assert.That(patch.Segments.Count, Is.EqualTo(1));
-
- 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));
- }
-
- [Test]
- public void Should_ReturnBinaryPatchSegment_When_ModifiedLongerThanOriginal()
- {
- // Arrange
- var original = new byte[] { 0x0 };
- var modified = new byte[] { 0x0, 0x1 };
-
- // Act
- var patchSource = BinaryPatchSource.Create(original, blockSize: 2);
-
- var patch = patchSource.Calculate(modified);
-
- // Assert
- Assert.That(patch, Is.Not.Null);
- Assert.That(patch.Segments, Is.Not.Empty);
- Assert.That(patch.Segments.Count, Is.EqualTo(1));
-
- var firstSegment = patch.Segments[0];
-
- Assert.That(firstSegment, Is.Not.Null);
-
- var binaryPatchSegment = firstSegment as DataPatchSegment;
-
- Assert.That(binaryPatchSegment, Is.Not.Null);
- Assert.That(binaryPatchSegment.Memory.Length, Is.EqualTo(2));
- }
-
- [Test]
- public void Should_ReturnBinaryPatchSegment_When_ModifiedLongerAndDifferent()
- {
- // Arrange
- var original = new byte[] { 0x0, 0x0 };
- var modified = new byte[] { 0x0, 0x1, 0x0 };
-
- // Act
- var patchSource = BinaryPatchSource.Create(original);
-
- var patch = patchSource.Calculate(modified);
-
- // Assert
- Assert.That(patch, Is.Not.Null);
- Assert.That(patch.Segments, Is.Not.Empty);
- Assert.That(patch.Segments.Count, Is.EqualTo(1));
-
- var firstSegment = patch.Segments[0];
-
- Assert.That(firstSegment, Is.Not.Null);
-
- var binaryPatchSegment = firstSegment as DataPatchSegment;
-
- Assert.That(binaryPatchSegment, Is.Not.Null);
- Assert.That(binaryPatchSegment.Memory.Length, Is.EqualTo(3));
- }
}
\ No newline at end of file
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}"
diff --git a/src/BitSoft.BinaryTools/Patch/BinaryPatch.cs b/src/BitSoft.BinaryTools/Patch/BinaryPatch.cs
index fc9a4cd..3bbd568 100644
--- a/src/BitSoft.BinaryTools/Patch/BinaryPatch.cs
+++ b/src/BitSoft.BinaryTools/Patch/BinaryPatch.cs
@@ -1,120 +1,252 @@
-using System;
-using System.Collections.Generic;
+using System;
+using System.Buffers;
using System.IO;
-using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
namespace BitSoft.BinaryTools.Patch;
-public sealed class BinaryPatch
+public static class BinaryPatch
{
- public int BlockSize { get; }
+ private static readonly ArrayPool Pool = ArrayPool.Shared;
+
+ 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);
- public IReadOnlyList Segments { get; }
+ 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));
- public static Encoding DefaultEncoding => Encoding.UTF8;
+ var blockInfoContainer = await CalculateHashesAsync(source, blockSize, cancellationToken);
- public BinaryPatch(IReadOnlyList segments, int blockSize)
- {
- if (blockSize <= 0) throw new ArgumentOutOfRangeException(nameof(blockSize));
+ using var writer = new PatchWriter(output);
- Segments = segments ?? throw new ArgumentNullException(nameof(segments));
- BlockSize = blockSize;
- }
+ await writer.WriteHeaderAsync(blockSize: blockSize, cancellationToken);
- public void Write(Stream target, Encoding? encoding = null)
- {
- ArgumentNullException.ThrowIfNull(target);
+ var bufferLength = blockSize * 2;
+ var buffer = Pool.Rent(minimumLength: bufferLength);
+ try
+ {
+ var length = await modified.ReadAsync(buffer.AsMemory(start: 0, length: bufferLength), cancellationToken);
+ if (length == 0)
+ return;
- if (!target.CanWrite)
- throw new ArgumentException("The target stream must be writable.", nameof(target));
+ const int NotDefined = -1;
- encoding ??= DefaultEncoding;
+ var segmentStart = NotDefined;
+ var position = 0;
- using var binaryWriter = new BinaryWriter(target, encoding, leaveOpen: true);
+ RollingHash rollingHash = default;
+ var resetHash = true;
- binaryWriter.Write(ProtocolConst.ProtocolVersion);
- binaryWriter.Write(BlockSize);
+ while (true)
+ {
+ 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;
+ }
+
+ var block = blockInfoContainer.Match(rollingHash);
+
+ if (block is null)
+ {
+ if (length <= blockSize)
+ {
+ var memory = buffer.AsMemory(start: position, length: length);
+ await writer.WriteDataAsync(memory, cancellationToken);
+ position = 0;
+ break;
+ }
+
+ if (segmentStart == NotDefined)
+ {
+ segmentStart = position;
+ }
+ else if (position - segmentStart + 1 == blockSize)
+ {
+ var memory = buffer.AsMemory(start: segmentStart, length: position - segmentStart + 1);
+ await writer.WriteDataAsync(memory, cancellationToken);
+
+ buffer
+ .AsSpan(start: position + 1, length: bufferLength - position - 2)
+ .CopyTo(buffer.AsSpan(start: 0));
+
+ segmentStart = NotDefined;
+ resetHash = true;
+
+ break;
+ }
+
+ position += 1;
+
+ if (position == length)
+ {
+ var memory = buffer.AsMemory(start: segmentStart, length: position - segmentStart);
+ await writer.WriteDataAsync(memory, cancellationToken);
+ position = 0;
+ break;
+ }
+
+ 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
+ {
+ if (segmentStart != NotDefined)
+ {
+ var memory = buffer.AsMemory(start: segmentStart, length: position - segmentStart);
+ await writer.WriteDataAsync(memory, cancellationToken);
+ segmentStart = NotDefined;
+ }
+
+ await writer.WriteCopyAsync(
+ blockIndex: block.BlockIndex,
+ blockLength: block.Length,
+ cancellationToken: cancellationToken
+ );
+
+ buffer
+ .AsSpan(start: position + block.Length, length: bufferLength - position - block.Length - 1)
+ .CopyTo(buffer.AsSpan(start: 0));
+
+ resetHash = true;
+
+ break;
+ }
+ }
- for (var i = 0; i < Segments.Count; i++)
- {
- var segment = Segments[i];
+ length = await modified.ReadAsync(
+ buffer.AsMemory(start: position, length: bufferLength - position - 1),
+ cancellationToken: cancellationToken
+ );
- var segmentType = segment switch
- {
- CopyPatchSegment => ProtocolConst.SegmentTypes.CopyPatchSegment,
- DataPatchSegment => ProtocolConst.SegmentTypes.DataPatchSegment,
- _ => throw new InvalidOperationException($"Invalid segment type '{segment.GetType()}'.")
- };
- binaryWriter.Write(segmentType);
+ length += position;
+ position = 0;
- 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);
+ if (length == 0)
break;
}
}
+ finally
+ {
+ Pool.Return(buffer);
+ }
- binaryWriter.Write(ProtocolConst.SegmentTypes.EndPatchSegment);
+ await writer.CompleteAsync(cancellationToken);
}
- public static BinaryPatch Read(Stream source, Encoding? encoding = null)
+ public static async ValueTask ApplyAsync(
+ Stream source,
+ Stream patch,
+ Stream output,
+ CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(source);
+ ArgumentNullException.ThrowIfNull(patch);
+ ArgumentNullException.ThrowIfNull(output);
if (!source.CanRead)
- throw new ArgumentException("The source stream must be readable.", nameof(source));
+ 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);
+
+ while (await reader.ReadAsync(cancellationToken))
+ {
+ switch (reader.Segment)
+ {
+ case DataPatchSegment dataPatchSegment:
+ await output.WriteAsync(dataPatchSegment.Data, cancellationToken);
+ break;
+ case CopyPatchSegment copyPatchSegment:
+ var targetPosition = blockSize * copyPatchSegment.BlockIndex;
+ source.Seek(targetPosition, SeekOrigin.Begin);
+ var buffer = Pool.Rent(copyPatchSegment.BlockLength);
+ try
+ {
+ 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
+ {
+ Pool.Return(buffer);
+ }
- encoding ??= DefaultEncoding;
+ break;
+ default:
+ throw new NotSupportedException();
+ }
+ }
+ }
- using var binaryReader = new BinaryReader(source, encoding, leaveOpen: true);
+ private static async ValueTask CalculateHashesAsync(
+ Stream source,
+ int blockSize,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(source);
- var protocolVersion = binaryReader.ReadInt32();
- if (protocolVersion > ProtocolConst.ProtocolVersion)
- throw new InvalidOperationException($"Invalid protocol version '{protocolVersion}'.");
+ if (!source.CanRead)
+ throw new ArgumentException("source stream must be readable.", nameof(source));
- var blockSize = binaryReader.ReadInt32();
+ var blockInfoContainer = new BlockInfoContainer();
- var segments = new List();
+ var blockIndex = 0;
- while (true)
+ var buffer = Pool.Rent(blockSize);
+ try
{
- var segmentType = binaryReader.ReadByte();
- IBinaryPatchSegment? segment = null;
- switch (segmentType)
+ while (true)
{
- case ProtocolConst.SegmentTypes.CopyPatchSegment:
- {
- var blockIndex = binaryReader.ReadInt32();
- var length = binaryReader.ReadInt32();
- segment = new CopyPatchSegment(blockIndex: blockIndex, length: length);
+ var length = await source.ReadAsync(buffer.AsMemory(start: 0, length: blockSize), cancellationToken);
+ if (length == 0)
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
+
+ var hash = RollingHash.Create(buffer.AsSpan(start: 0, length: length));
+
+ blockInfoContainer.Process(hash: hash, blockIndex: blockIndex, blockLength: length);
+
+ if (length < blockSize)
break;
- default:
- throw new InvalidOperationException($"Invalid segment type Id '{segmentType}'.");
- }
- if (segment is not null)
- segments.Add(segment);
- else
- break;
+ blockIndex += 1;
+ }
+ }
+ finally
+ {
+ Pool.Return(buffer);
}
- return new BinaryPatch(segments, blockSize);
+ return blockInfoContainer;
}
-}
\ No newline at end of file
+}
diff --git a/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs b/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs
deleted file mode 100644
index 7deeb4d..0000000
--- a/src/BitSoft.BinaryTools/Patch/BinaryPatchSource.cs
+++ /dev/null
@@ -1,154 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-
-namespace BitSoft.BinaryTools.Patch;
-
-public sealed class BinaryPatchSource
-{
- private readonly BlockInfoContainer _blockInfoContainer;
- private readonly int _blockSize;
-
- 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)
- {
- var hashes = CalculateHashes(original, blockSize);
- return new BinaryPatchSource(hashes, blockSize);
- }
-
- public BinaryPatch Calculate(ReadOnlyMemory modified)
- {
- var maxSize = modified.Length / _blockSize;
- var segments = new List(capacity: maxSize);
-
- var modifiedSpan = modified.Span;
- var initialSpan = modified.Span[..Math.Min(modifiedSpan.Length, _blockSize)];
- var rollingHash = RollingHash.Create(initialSpan);
-
- const int NotDefined = -1;
-
- var segmentStart = NotDefined;
- var position = 0;
-
- while (position < modifiedSpan.Length)
- {
- var checksum = rollingHash.GetChecksum();
-
- var block = _blockInfoContainer.Match(rollingHash);
-
- 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;
- }
-
- var copyPatchSegment = new CopyPatchSegment(blockIndex: block.BlockIndex, length: block.Length);
- segments.Add(copyPatchSegment);
- position += block.Length;
-
- if (position >= modifiedSpan.Length)
- break;
-
- var span = modifiedSpan.Slice(
- start: position,
- length: Math.Min(modifiedSpan.Length - position, _blockSize)
- );
- rollingHash = RollingHash.Create(span);
- }
- else
- {
- if (segmentStart == NotDefined)
- segmentStart = position;
-
- position += 1;
-
- if (position == modifiedSpan.Length)
- {
- if (segmentStart != NotDefined)
- {
- var dataPatchSegment = new DataPatchSegment(
- memory: modified.Slice(start: segmentStart, length: position - segmentStart)
- );
- segments.Add(dataPatchSegment);
- }
-
- break;
- }
-
- var removedByte = modifiedSpan[position - 1];
- var addedByte =
- position + _blockSize <= modifiedSpan.Length
- ? modifiedSpan[position + _blockSize - 1]
- : modifiedSpan[modifiedSpan.Length - 1];
-
- rollingHash.Update(removed: removedByte, added: addedByte);
- }
- }
-
- return new BinaryPatch(segments, _blockSize);
- }
-
- public static void Apply(ReadOnlyMemory original, BinaryPatch patch, Stream target)
- {
- ArgumentNullException.ThrowIfNull(patch);
- ArgumentNullException.ThrowIfNull(target);
-
- foreach (var segment in patch.Segments)
- {
- switch (segment)
- {
- case DataPatchSegment dataPatchSegment:
- target.Write(dataPatchSegment.Memory.Span);
- break;
- case CopyPatchSegment copyPatchSegment:
- var slice = original.Slice(
- start: copyPatchSegment.BlockIndex * patch.BlockSize,
- length: copyPatchSegment.Length
- );
- target.Write(slice.Span);
- break;
- default:
- throw new NotSupportedException();
- }
- }
- }
-
- private static BlockInfoContainer CalculateHashes(ReadOnlyMemory original, int blockSize)
- {
- var blockInfoContainer = new BlockInfoContainer(length: original.Length, blockSize: blockSize);
-
- var blockIndex = 0;
-
- while (true)
- {
- 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);
-
- var hash = RollingHash.Create(slice.Span);
-
- blockInfoContainer.Process(hash: hash, blockIndex: blockIndex, blockLength: length);
-
- if (length < blockSize)
- break;
-
- blockIndex += 1;
- }
-
- return blockInfoContainer;
- }
-}
\ No newline at end of file
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/CopyPatchSegment.cs b/src/BitSoft.BinaryTools/Patch/CopyPatchSegment.cs
index e9aaa6a..c42a4ff 100644
--- a/src/BitSoft.BinaryTools/Patch/CopyPatchSegment.cs
+++ b/src/BitSoft.BinaryTools/Patch/CopyPatchSegment.cs
@@ -1,14 +1,8 @@
namespace BitSoft.BinaryTools.Patch;
-public sealed class CopyPatchSegment : IBinaryPatchSegment
+internal sealed class CopyPatchSegment(int blockIndex, int blockLength) : IPatchSegment
{
- public int BlockIndex { get; }
+ public int BlockIndex { get; } = blockIndex;
- public int Length { get; }
-
- public CopyPatchSegment(int blockIndex, int length)
- {
- BlockIndex = blockIndex;
- Length = length;
- }
-}
\ No newline at end of file
+ public int BlockLength { get; } = blockLength;
+}
diff --git a/src/BitSoft.BinaryTools/Patch/DataPatchSegment.cs b/src/BitSoft.BinaryTools/Patch/DataPatchSegment.cs
index 32782dc..c84dc1e 100644
--- a/src/BitSoft.BinaryTools/Patch/DataPatchSegment.cs
+++ b/src/BitSoft.BinaryTools/Patch/DataPatchSegment.cs
@@ -2,12 +2,7 @@
namespace BitSoft.BinaryTools.Patch;
-public sealed class DataPatchSegment : IBinaryPatchSegment
+internal sealed class DataPatchSegment(ReadOnlyMemory data) : IPatchSegment
{
- public ReadOnlyMemory Memory { get; }
-
- public DataPatchSegment(ReadOnlyMemory memory)
- {
- Memory = memory;
- }
-}
\ No newline at end of file
+ public ReadOnlyMemory Data { get; } = data;
+}
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/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 46c38bb..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 PatchBlockInfo(int blockIndex, uint hash, int length)
- {
- BlockIndex = blockIndex;
- Hash = hash;
- Length = length;
- }
+ public int BlockIndex { get; } = blockIndex;
- public int BlockIndex { get; }
+ public uint Hash { get; } = hash;
- public uint Hash { get; }
-
- public int Length { get; }
-}
\ 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
new file mode 100644
index 0000000..5e22ac9
--- /dev/null
+++ b/src/BitSoft.BinaryTools/Patch/PatchReader.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Buffers;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace BitSoft.BinaryTools.Patch;
+
+internal sealed class PatchReader : IDisposable
+{
+ private readonly BinaryReader _reader;
+
+ private static readonly ArrayPool Pool = ArrayPool.Shared;
+ private byte[]? _buffer;
+
+ public IPatchSegment? Segment { get; private set; }
+
+ public PatchReader(Stream source)
+ {
+ ArgumentNullException.ThrowIfNull(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);
+ }
+}
diff --git a/src/BitSoft.BinaryTools/Patch/PatchWriter.cs b/src/BitSoft.BinaryTools/Patch/PatchWriter.cs
new file mode 100644
index 0000000..723cbdb
--- /dev/null
+++ b/src/BitSoft.BinaryTools/Patch/PatchWriter.cs
@@ -0,0 +1,55 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace BitSoft.BinaryTools.Patch;
+
+internal sealed class PatchWriter : IDisposable
+{
+ private readonly BinaryWriter _writer;
+
+ public PatchWriter(Stream output)
+ {
+ ArgumentNullException.ThrowIfNull(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();
+ }
+}
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;