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;