diff --git a/.gitignore b/.gitignore index 588ce7f..a9aa3f4 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ mono_crash.* # Build results +*/compiledAot/* [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ diff --git a/README.md b/README.md index 370532b..e33f389 100644 --- a/README.md +++ b/README.md @@ -1,104 +1,431 @@ -# SharpHDiffPatch +# SharpHPatchZ [![NuGet Downloads](https://img.shields.io/nuget/dt/SharpHDiffPatch.Core.svg?style=flat-square)](https://www.nuget.org/packages/SharpHDiffPatch.Core/) [![NuGet version](https://img.shields.io/nuget/v/SharpHDiffPatch.Core.svg?style=flat-square)](https://www.nuget.org/packages/SharpHDiffPatch.Core/) -**SharpHDiffPatch** is a patching library for HDiffPatch format written in C#, purposedly as a port of **HPatchZ** implementation (from [**HDiffPatch** by **housisong**](https://github.com/sisong/HDiffPatch)). This project doesn't support making diff file and only works for patching. +**SharpHPatchZ** (formerly SharpHDiffPatch) is a patching library for HDiffPatch format written in C#, purposedly as a port of **HPatchZ** implementation (from [**HDiffPatch** by **housisong**](https://github.com/sisong/HDiffPatch)). This project doesn't support making a diff file and only works for patching. Supporting file and directory patching with these compression formats: - BZip2 - Deflate -- Zstd -- LZMA2 (not LZMA) +- ZStandard +- LZMA +- LZMA2 - No Compression. -Unfortunately, the **``HDIFFSF20``** (Single Compressed) format is still unsupported. But we are planning to add it in the future. +HDIFFSF20 and HDIFFW26 format is planned to be supported in later v3.0 releases. -This project is used as a part submodule of our main project: [**Collapse Launcher**](https://github.com/CollapseLauncher). +This project is used as a part of a submodule, widely used within our main project: [**Collapse Launcher**](https://github.com/CollapseLauncher) and [**Hi3Helper.Sophon**](https://github.com/CollapseLauncher/Hi3Helper.Sophon). -# Usage Example -## Patching with a simple progress indicator +# Supported Target Frameworks +Currently, SharpHPatchZ supports for a broad .NET Target Frameworks (TFM) and .NET Standard 2.0 compatible frameworks below: + +| TFM | Version | +|-----|-------| +| .NET Framework | 4.6.1 or above [1][2] | +| .NET Core | 6 or above [3] | +| Mono | 5.4 or above, 6.4 or above (Untested) [1][2] | +| Unity | 2018.1 or above (Untested) [1][2] | + +### Sidenotes: +- **[1]** SIMD-based RLE Addition is not supported +- **[2]** ZStandard decompression uses managed-port instead. This applies for any **non .NET Core TFMs** and **any platform other than:** **Linux x64/arm64** and **Windows x64/arm64**. +- **[3]** For .NET 11 or above, the built-in `ZstandardStream` will be used for decompression instead. This should be supported for any platform (including Android, Windows, Linux, macOS, iOS, etc.) + +# Usage Examples +## A. Basic Patching Usage with Progress Output ```CSharp -using SharpHDiffPatch.Core; -using SharpHDiffPatch.Core.Event; - -string oldPath = "C:\\test\\Music1.pck"; -string diffPath = "C:\\test\\Music1.pck.hdiff"; -string newPath = "C:\\test\\Music1.pck.new"; - -// Initialize the patcher instance -HDiffPatch patcher = new HDiffPatch(); -// Set the verbosity of the logging -// Available Options: Quiet, Info (default), Verbose, Debug -HDiffPatch.LogVerbosity = Verbosity.Verbose; - -// Subscribe an event listener to logging -EventListener.LoggerEvent += EventListener_LoggerEvent; -// Subscribe an event listener to patching progress -EventListener.PatchEvent += EventListener_PatchEvent; - -// Initialize the diff file -patcher.Initialize(diffPath); -// Start the patching process -// This method has some arguments you can tweak as below: -// Patch(string inputPath, string outputPath, bool useBufferedPatch, -// CancellationToken token = default, bool useFullBuffer = false, -// bool useFastBuffer = false) -// -// Description: -// - inputPath -> Path of the old/source file/folder. -// - outputPath -> Path of the new/target file/folder. -// - useBufferedPatch -> Use array-based buffer for RLE Control and Code clips. -// - token -> Cancellation token. -// - useFullBuffer -> Buffer the RLE New Data to the MemoryStream. -// - useFastBuffer -> Buffer the RLE Control and Code clips to ArrayPool. -patcher.Patch(inputPath, outputPath, true, default, false, true); - -// Unsubscribe an event listener to logging -EventListener.LoggerEvent -= EventListener_LoggerEvent; -// Unsubscribe an event listener to patching progress -EventListener.PatchEvent -= EventListener_PatchEvent; - -// Implement logging listener -private void EventListener_LoggerEvent(object? sender, LoggerEvent e) +using System; +using SharpHPatchZ; +using SharpHPatchZ.Header; + +namespace Example; + +public class Program { - string label = e.LogLevel switch + private const string InputPath = @"G:\HDiffTest\Hi3SEA"; + private const string PatchPath = @"G:\HDiffTest\Hi3SEAtoCNExecOnly.lzma.diff"; + private const string OutputPath = @"G:\HDiffTest\Hi3CN-dotnet"; + + public static void Main() { - Verbosity.Info => $"[Info] ", - Verbosity.Verbose => $"[Verbose] ", - Verbosity.Debug => $"[Debug] ", - _ => "" - }; - Console.WriteLine($"{label}{e.Message}"); + // Create a callback struct and pass the `UpdateProgress` method to the create method. + ProgressCallback progressCallback = ProgressCallback.CreateFromManaged(UpdateProgress); + + // The `HDiffInfo` info context must be used within `using` scope or by disposing the struct manually. + // Undisposed info context will cause a memory leak as it uses unmanaged memory under the hood. + + // Pass the patch file path into the `HPatch.CreateInstance()` method to create the info context. + using HDiffInfo info = HPatch.CreateInstance(PatchPath); + + // Pass the `HDiffInfo` info context to the `HPatch.Patch()` method to start the patching process. Also pass the Input and Output Path, and the `progressCallback` + PatchResult patchResult = HPatch.Patch(info, PatchPath, InputPath, OutputPath, progressCallback: progressCallback); + + // If the patch process is not successful, throw the exception. + if (!patchResult) + throw patchResult.Exception ?? new Exception(); + } + + private static void UpdateProgress(long totalProcessed, long totalSize, int written) + { + double percent = Math.Round(totalProcessed / (double)totalSize * 100, 2); + Console.Write( + $"Patching: {percent}% | " + + $"{SummarizeSizeSimple(totalProcessed)}/{SummarizeSizeSimple(totalSize)} \r"); + } + + private static readonly string[] SizeSuffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + + private static string SummarizeSizeSimple(double value, int decimalPlaces = 2) + { + byte mag = (byte)Math.Log(value, 1000); + + return $"{Math.Round(value / (1L << (mag * 10)), decimalPlaces)} {SizeSuffixes[mag]}"; + } +} +``` + +## B. Basic Patching Usage with Adjusted Options +```CSharp +// There are few `PatchOptions` templates you can use. +// - PatchOptions.BigBuffer +// Generally the fastest, but uses numerous amount of buffer to process the patch. +// ParallelThreads = (Auto) determines the max. amount of patch worker used per thread. +// ReaderBufferSize = 1 MiB/reader +// CopyBufferSize = 128 KiB/copy file job (Only being used if the patch is a directory patch (HDiff19 type)) +// PatchWorkerBufferSize = 16 MiB/patch worker +// UseSIMD = true (only available for .NET 6 or above) +// +// - PatchOptions.Default +// Moderately faster, sometimes better for small patch. Uses modest amount of buffer to process the patch. +// ParallelThreads = (Auto) determines the max. amount of patch worker used per thread. +// ReaderBufferSize = 64 KiB/reader +// CopyBufferSize = 16 KiB/copy file job (Only being used if the patch is a directory patch (HDiff19 type)) +// PatchWorkerBufferSize = 1 MiB/patch worker +// UseSIMD = true (only available for .NET 6 or above) +// +// - PatchOptions.SmallBuffer +// Slower but uses the smallest buffer size possible. +// ParallelThreads = (Auto) determines the max. amount of patch worker used per thread. +// ReaderBufferSize = 4 KiB/reader +// CopyBufferSize = 4 KiB/copy file job (Only being used if the patch is a directory patch (HDiff19 type)) +// PatchWorkerBufferSize = 128 KiB/patch worker +// UseSIMD = true (only available for .NET 6 or above) +// +// - PatchOptions.OptimizeForHDD +// Equally the same as Default but spawn only one patch worker for better sequential write performance on HDD and to avoid +// heavy random seek. +// ParallelThreads = 1 (Spawn only one patch worker at a time for sequential write to the output.) +// ReaderBufferSize = 64 KiB/reader +// CopyBufferSize = 16 KiB/copy file job (Only being used if the patch is a directory patch (HDiff19 type)) +// PatchWorkerBufferSize = 1 MiB/patch worker +// UseSIMD = true (only available for .NET 6 or above) +PatchOptions patchOptions = PatchOptions.BigBuffer; + +// Create the info context from the patch file path. +using HDiffInfo info = HPatch.CreateInstance(PatchPath); + +// Pass the info context, patch patch, input path and output path, as well as the `patchOptions`. +PatchResult patchResult = HPatch.Patch(info, PatchPath, InputPath, OutputPath, options: patchOptions); +``` + +## C. Uses asychronous version of the method. +```CSharp +private static async Task ProcessPatch( + string patchPath, + string inputPath, + string outputPath, + PatchOptions options, + CancellationToken token) +{ + // Creates the info context asynchronously from the patch path. + using HDiffInfo info = await HPatch.CreateInstanceAsync(patchPath, token); + + // Starts the patch process asynchronously, pass the arguments as usual. + PatchResult patchResult = await HPatch.PatchAsync(info, patchPath, inputPath, outputPath, options: options, token: token); +} +``` + +## D. Uses a `Stream` Factory method to for the patch file. +### Synchronous version: +```CSharp +private static void ProcessPatch(string patchPath, string inputPath, string outputPath, PatchOptions options) +{ + // Create the info context from a `Stream` factory method by `CreateStream()`. + using HDiffInfo info = HPatch.CreateInstance(CreateStream); + + // Start the patch process by passing the `CreateStream()` factory method to the `HPatch.Patch()`. + PatchResult patchResult = HPatch.Patch(info, CreateStream, inputPath, outputPath, options: options); + + if (!patchResult) + throw patchResult.Exception ?? new Exception(); + + return; + + // This method is used to produce the `Stream` of the patch file starting from the specified position/offset of the file. + // The `position` argument here is necessary as it's used to specify where the data will start to be read by the patching methods. + (Stream stream, bool leaveOpen) CreateStream(long position) + { + // Open the patch `Stream` with shared Read operation. The `Stream` must be created with `FileShare.Read` as this method + // will be called to produce multiple `Stream` instances with shared Read operations. + // + // This is important to note that we don't use `using` keyword here as we don't want to dispose the `Stream` instance + // after leaving this method. We want the callee to dispose the `Stream` instance instead later by setting `leaveOpen` + // to `false` under the returned value below. + FileStream stream = File.Open(patchPath, FileMode.Open, FileAccess.Read, FileShare.Read); + + // The `Stream` instance must be seek-ed to the certain offset provided by the `position` argument. + stream.Position = position; + + // Return the `Stream` instance and set the `leaveOpen` to `false` to tell the callee to + // dispose this created `Stream` instance after use. + return (stream, false); + } +} +``` + +### Asynchronous version: +The `async` version is basically the same as the sychronous version but with a few adjustments onto the `Stream` factory method where it receives `CancellationToken` from the callee method within the patch processing method. +```CSharp +private static async Task ProcessPatch(string patchPath, string inputPath, string outputPath, PatchOptions options, CancellationToken token) +{ + // Create the info context from a `Stream` factory method by `CreateStreamAsync()`. + using HDiffInfo info = await HPatch.CreateInstanceAsync(CreateStreamAsync, token); + + // Start the patch process by passing the `CreateStreamAsync()` factory method to the `HPatch.PatchAsync()`. + PatchResult patchResult = await HPatch.PatchAsync(info, CreateStreamAsync, inputPath, outputPath, options: options, token: token); + + if (!patchResult) + throw patchResult.Exception ?? new Exception(); + + return; + + // This method is used to produce the `Stream` of the patch file starting from the specified position/offset of the file. + // The `position` argument here is necessary as it's used to specify where the data will start to be read by the patching methods. + ValueTask<(Stream stream, bool leaveOpen)> CreateStreamAsync(long position, CancellationToken calleeToken) + { + // Open the patch `Stream` with shared Read operation. The `Stream` must be created with `FileShare.Read` as this method + // will be called to produce multiple `Stream` instances with shared Read operations. + // + // This is important to note that we don't use `using` keyword here as we don't want to dispose the `Stream` instance + // after leaving this method. We want the callee to dispose the `Stream` instance instead later by setting `leaveOpen` + // to `false` under the returned value below. + FileStream stream = File.Open(patchPath, FileMode.Open, FileAccess.Read, FileShare.Read); + + // The `Stream` instance must be seek-ed to the certain offset provided by the `position` argument. + stream.Position = position; + + // Return the `Stream` instance and set the `leaveOpen` to `false` to tell the callee to + // dispose this created `Stream` instance after use. + // + // Another thing to note on this asynchronous version that since we don't await any method here, we can + // just pass the result of the `ValueTask` instead. For the fully asynchronous example, you can go to + // the next example below. + return new ValueTask<(Stream stream, bool leaveOpen)>((stream, false)); + } } +``` + +## E. Uses a `Stream` Factory to open Patch file from a remote URL (HTTP). +**Yes, you heard it right.** Thanks to the power of "re-writing the code instead of fixing the existing ones", we re-think to make the read of the patch from a remote `Stream` possible. In this example, we utilizes a fully asynchronous method version of the methods. This demo is basically identical as previous one but we adjust the inner `CreateStreamAsync` method to produce the `Stream` instance from an `HttpClient` as the methods under the `HttpClient` are mostly asynchronous. -// Implement patching progress listener -private void EventListener_PatchEvent(object? sender, PatchEvent e) +```CSharp +private static async Task ProcessPatchFromRemote(HttpClient client, + string patchUrl, + string inputPath, + string outputPath, + PatchOptions options, + CancellationToken token) { - Console.Write($"Patching: {e.ProgressPercentage}% | {SummarizeSizeSimple(e.CurrentSizePatched)}/{SummarizeSizeSimple(e.TotalSizeToBePatched)} @{SummarizeSizeSimple(e.Speed)}/s \r"); + // Create the info context from a `Stream` factory method by `CreateStreamAsync()`. + using HDiffInfo info = await HPatch.CreateInstanceAsync(CreateStreamAsync, token); + + // Start the patch process by passing the `CreateStreamAsync()` factory method to the `HPatch.PatchAsync()`. + PatchResult patchResult = await HPatch.PatchAsync(info, CreateStreamAsync, inputPath, outputPath, options: options, token: token); + + if (!patchResult) + throw patchResult.Exception ?? new Exception(); + + return; + + // This factory method produces a remote `Stream` instance from a remote URL with specified position/offset. + // This method is now fully asynchronous than the previous demos. + async ValueTask<(Stream stream, bool leaveOpen)> CreateStreamAsync(long position, CancellationToken calleeToken) + { + // Creates a `GET` request message to the HTTP server with specified URL. + // Also, the request message must provide a `content-range` header by specifying `RangeHeaderValue` + // to the `HttpRequestMessage.Headers.Range` property. The `null` after the `position` here means + // to: "Get a slice of the remote `Stream` starting from this `position` to the rest of the file." + HttpRequestMessage request = new(HttpMethod.Get, patchUrl); + request.Headers.Range = new RangeHeaderValue(position, null); + + // Send the `GET` request message to the HTTP server, then get the response of the request. + // The `HttpCompletionOption.ResponseHeadersRead` here is important as we don't want to pre-cache + // the entire response data to the memory temporarily (which took much longer). Instead, we just + // want it on-demand and read the rest of the data we only wanted. + HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, calleeToken); + + // This method is actually optional to call, but it's better for us to call it upfront to + // ensure that the response produces `2xx` response (OK/Continue) and return the actual patch data + // instead of an error message. + // + // If non-`2xx` response is returned, then throw for us. + response.EnsureSuccessStatusCode(); + + // Open the remote `Stream` instance and pass the `leaveOpen` to `false` to tell the callee to + // dispose the `Stream` instance after use (and so, disposing the `HttpRequestMessage` and + // `HttpResponseMessage` for us automatically). + return (await response.Content.ReadAsStreamAsync(calleeToken), false); + } } ``` -## Patching Kuro directory diffs +## F. Initialize Kuro Games HDiff19 Patch Format +Kuro Games recently produces a slightly modified version of HDiff19 to be used on the patching process of their game, Wuthering Waves. This format slighty changes how the Directory Patch Information is structurized. -Some Kuro Games directory diffs use an extended directory header containing old-reference sizes and new-reference hashes. Select the Kuro format after initializing the diff and before calling `Patch`: +Instead of using this regular format: +``` +... previous struct +- Input Path Entry Array (string) +- Output Path Entry Array (string) +- Input Files Index Array (int) +- Output Files Index Array (int) +- Output Files Sizes Array (long) +- Same File Path Index Pair Array (int, int) +... next struct +``` +They added two additional structs: +``` +... previous struct +- Input Path Entry Array (string) +- Output Path Entry Array (string) +- Input Files Index Array (int) +- Output Files Index Array (int) +- Input Files Sizes Array (long) < Exists on Kuro Games format, used for input file size sanity > +- Output Files Sizes Array (long) +- Output Files Hashes Array (long) < Exists on Kuro Games format, used for final output file integrity > +- Same File Path Index Pair Array (int, int) +... next struct +``` + +Another to mention that the Kuro Games format still uses the same signature magic, which is `HDiff19`, we can't make it distinctly different from normal ones. So in order to initialize the patch file, you have to manually create an additional `InitializeOptions` and set the `IsKuroGamesHDiff` field to `true`, then pass the struct into the `HPatch.CreateInstance()` or `HPatch.CreateInstanceAsync` method (depends on your use case). +Here's the example of the usage: ```CSharp -using SharpHDiffPatch.Core; +private static void ProcessPatch(string patchPath, + string inputPath, + string outputPath, + PatchOptions options) +{ + // Create the initialization option and set `IsKuroGamesHDiff` to `true`. + InitializeOptions initializeOptions = new() + { + IsKuroGamesHDiff = true + }; + + // Pass the initialization option to the `CreateInstance()` method. + using HDiffInfo info = HPatch.CreateInstance(patchPath, initializeOptions); -HDiffPatch patcher = new HDiffPatch(); -patcher.Initialize(diffPath); -patcher.DirPatchFormat = DirectoryPatchFormat.Kuro; -patcher.Patch(inputPath, outputPath, true, default, false, true); + // Start the patching process as usual. + PatchResult patchResult = HPatch.Patch(info, patchPath, inputPath, outputPath, options: options); + + if (!patchResult) + throw patchResult.Exception ?? new Exception(); +} ``` +_Thanks to @Cryotechnic for the initial implementaion on the V2 codebase._ + +### For Kuro Games developer: -`DirectoryPatchFormat.Standard` remains the default. Use `Kuro` only for directory diffs produced with Kuro's extended header; the patcher also validates that each old reference file has the size recorded in that header. +*I think it's better for you to make a different signature magic (for example: `HDiff19Kuro` or something) instead of just adding an arbitrary structs inside of the file so it's easier for us to automatically parse your format. Also, don't forget to ask the permission to the original developer of the format (housisong) that you slightly changed their format for your own purposes (**And... don't forget to give them a credit on your launcher license file or something**)* 😉 -## Get the New file size from diff file. +~ @neon-nyan + +### G. Use Miscellaneous Utilities +We have few utility methods which enables you to play with the `HDiffInfo` info context struct and gets some information about the patch file. + +#### Example 1: Get the PatchMetadata using `HPatch.TryGetPatchMetadata()` +```CSharp +// Creates an `HDiffInfo` info context from a patch file path. +HDiffInfo info = HPatch.CreateInstance(PatchPath); + +try +{ + // Try to get the `PatchMetadata` out of `HDiffInfo` info context. + if (!HPatch.TryGetPatchMetadata(ref info, out PatchMetadata patchMetadata)) + { + throw new InvalidOperationException("Cannot get PatchMetadata"); + } + + // Prints some info from the `PatchMetadata` struct. + Console.WriteLine($""" + Old Data Size: {patchMetadata.DiffOldSize} + New Data Size: {patchMetadata.DiffNewSize} + RLE Cover Info Count: {patchMetadata.CoverDataCount} + """); +} +finally +{ + // Dispose the info context struct to release the unmanaged resources + info.Dispose(); +} +``` + +#### Example 2: Get the DirectoryPatchMetadata and prints the Input and Output Path List ```CSharp -using SharpHDiffPatch.Core; +// Creates an `HDiffInfo` info context from a patch file path. +HDiffInfo info = HPatch.CreateInstance(PatchPath); -string diffPath = "C:\\test\\Music1.pck.hdiff"; -long newFileSize = HDiffPatch.GetHDiffNewSize(diffPath); +try +{ + // Try to get the `PatchMetadata` out of `HDiffInfo` info context. + if (!HPatch.TryGetDirectoryPatchMetadata(ref info, out DirectoryPatchMetadata directoryPatchMetadata)) + { + Console.WriteLine("File is not an HDiff19 Directory patch format."); + return; + } + + // Print data from DirectoryPatchMetadata. + // NOTE: As some of these methods are unsafe, you must enable to true in your project file. + unsafe + { + // Creates a `Span` from an unmanaged `InputPathListP` field + Span inputPaths = HPatch.TryGetUnmanagedArraySpan(directoryPatchMetadata.InputPathListP); + Console.WriteLine("Input paths:"); + for (int i = 0; i < inputPaths.Length; i++) + { + // Prints the `Utf16UnmanagedString` directly as it implicitly converts itself to a managed `string` type. + Console.WriteLine($" - {inputPaths[i]}"); + } -Console.WriteLine($"The new file size is: {newFileSize} bytes"); + // Creates a `Span` from an unmanaged `OutputPathListP` field + Span outputPaths = HPatch.TryGetUnmanagedArraySpan(directoryPatchMetadata.OutputPathListP); + Console.WriteLine("Output paths:"); + for (int i = 0; i < outputPaths.Length; i++) + { + // Prints the `Utf16UnmanagedString` directly as it implicitly converts itself to a managed `string` type. + Console.WriteLine($" - {outputPaths[i]}"); + } + } +} +finally +{ + // Dispose the info context struct to release the unmanaged resources + info.Dispose(); +} ``` + +#### Example 3: Get the total Input/Output size from the patch path. +```CSharp +// Try to get the total size of the input or output files from a patch file path. +if (!HPatch.TryGetDiffSizeInfo(PatchPath, out long totalInputSize, out long totalOutputSize)) +{ + // If failed, throw. + throw new InvalidOperationException("Patch file might be invalid or unsupported"); +} + +// Print the total size info. +Console.WriteLine($""" + Total Old/Input File Size: {totalInputSize} + Total New/Output File Size: {totalOutputSize} + """); +``` \ No newline at end of file diff --git a/SharpHDiffPatch.Core/Binary/BinaryExtensions.cs b/SharpHDiffPatch.Core/Binary/BinaryExtensions.cs deleted file mode 100644 index e0c5a05..0000000 --- a/SharpHDiffPatch.Core/Binary/BinaryExtensions.cs +++ /dev/null @@ -1,282 +0,0 @@ -using System; -using System.Buffers; -using System.IO; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Text; -using SharpHDiffPatch.Core.Patch; - -namespace SharpHDiffPatch.Core.Binary; - -internal static class BinaryExtensions -{ -#if NETSTANDARD2_0 || !NET7_0_OR_GREATER - public static int ReadExactly(this Stream stream, byte[] buffer, int offset, int count) - { - int totalRead = 0; - while (totalRead < count) - { - int read = stream.Read(buffer, offset + totalRead, count - totalRead); - if (read == 0) return totalRead; - - totalRead += read; - } - - return totalRead; - } -#endif - - public static string ReadStringToNull(this Stream reader, int bufferSize = 512) - { - int i = 0; - - ArrayPool pool = ArrayPool.Shared; - byte[] stringBuffer = pool.Rent(bufferSize); - - try - { - int currentValue; - while (stringBuffer.Length > i && (currentValue = reader.ReadByte()) > 0) - { - stringBuffer[i++] = (byte)currentValue; - } - - return Encoding.UTF8.GetString(stringBuffer, 0, i); - } - finally - { - pool.Return(stringBuffer); - } - } - - public static int ReadInt7Bit(this Stream inputStream, int tagBit = 0, byte prevTagBit = 0) - { - bool isUseTagBit = tagBit != 0; - - byte code = isUseTagBit ? prevTagBit : (byte)inputStream.ReadByte(); - int value = code & ((1 << (7 - tagBit)) - 1); - - if ((code & (1 << (7 - tagBit))) == 0) return value; - do - { - if (value >> (4 * 4 - 7) != 0) return 0; - code = (byte)inputStream.ReadByte(); - value = (value << 7) | (code & ((1 << 7) - 1)); - } - while ((code & (1 << 7)) != 0); - return value; - } - - public static long ReadLong7Bit(this Stream inputStream, int tagBit = 0, byte prevTagBit = 0) - { - bool isUseTagBit = tagBit != 0; - - byte code = isUseTagBit ? prevTagBit : (byte)inputStream.ReadByte(); - long value = code & ((1 << (7 - tagBit)) - 1); - - if ((code & (1 << (7 - tagBit))) == 0) return value; - do - { - if (value >> (8 * 8 - 7) != 0) return 0; - code = (byte)inputStream.ReadByte(); - value = (value << 7) | (code & (((long)1 << 7) - 1)); - } - while ((code & (1 << 7)) != 0); - return value; - } - - public static long ReadLong7Bit(this byte[] inputBuffer, ref int offset, int tagBit = 0, byte prevTagBit = 0) - { - bool isUseTagBit = tagBit != 0; - - byte code = isUseTagBit ? prevTagBit : inputBuffer[offset++]; - long value = code & ((1 << (7 - tagBit)) - 1); - - if ((code & (1 << (7 - tagBit))) == 0) return value; - - do - { - if (value >> (8 * 8 - 7) != 0) return 0; - code = inputBuffer[offset++]; - value = (value << 7) | (code & (((long)1 << 7) - 1)); - } - while ((code & (1 << 7)) != 0); - return value; - } - - public static long ReadLong7Bit(this ReadOnlySpan inputBuffer, ref int offset, int tagBit = 0, byte prevTagBit = 0) - { - bool isUseTagBit = tagBit != 0; - - byte code = isUseTagBit ? prevTagBit : inputBuffer[offset++]; - long value = code & ((1 << (7 - tagBit)) - 1); - - if ((code & (1 << (7 - tagBit))) == 0) return value; - - do - { - if (value >> (8 * 8 - 7) != 0) return 0; - code = inputBuffer[offset++]; - value = (value << 7) | (code & (((long)1 << 7) - 1)); - } - while ((code & (1 << 7)) != 0); - return value; - } - - public static ref byte ReadLong7Bit(this ref byte inputBuffer, out long value, int tagBit = 0, byte prevTagBit = 0) - { - byte code = tagBit == 0 ? inputBuffer : prevTagBit; - value = code & ((1 << (7 - tagBit)) - 1); - - if (tagBit == 0) - { - inputBuffer = ref Unsafe.AddByteOffset(ref inputBuffer, 1); - } - - if ((code & (1 << (7 - tagBit))) == 0) - { - return ref inputBuffer; - } - - Calc: - if (value >> (8 * 8 - 7) != 0) - { - value = 0; - return ref inputBuffer; - } - code = inputBuffer; - value = (value << 7) | (code & (((long)1 << 7) - 1)); - inputBuffer = ref Unsafe.AddByteOffset(ref inputBuffer, 1); - if ((code & (1 << 7)) != 0) - { - goto Calc; - } - - return ref inputBuffer; - } - - public static bool ReadBoolean(this Stream stream) => stream.ReadByte() != 0; - - public static ref T AsRef(this byte[] coverBuffer, int coverHeaderOffset = 0) - => ref Unsafe.As(ref coverBuffer[coverHeaderOffset]); - - public static void GetPathsFromStream(this Stream reader, out string[] outputPaths, int bufferSize, int count) - { - byte[] buffer = ArrayPool.Shared.Rent(bufferSize); - - try - { - reader.ReadExactly(buffer, 0, bufferSize); - buffer.AsSpan().GetPathsFromBuffer(out outputPaths, count); - } - finally - { - ArrayPool.Shared.Return(buffer); - } - } - - public static unsafe void GetPathsFromBuffer(this Span buffer, out string[] outputPaths, int count) - { - outputPaths = new string[count]; - - int idx = 0, strIdx = 0; -#if NETSTANDARD2_0 || NET461_OR_GREATER - int len = 0; -#endif - fixed (byte* inputPtr = &MemoryMarshal.GetReference(buffer)) - { - do - { -#if !(NETSTANDARD2_0 || NET461_OR_GREATER) - ReadOnlySpan inputSpanned = - MemoryMarshal.CreateReadOnlySpanFromNullTerminated(inputPtr + idx); - idx += inputSpanned.Length + 1; - outputPaths[strIdx++] = Encoding.UTF8.GetString(inputSpanned); -#else - if (*(inputPtr + idx++) == 0) - { - outputPaths[strIdx++] = Encoding.UTF8.GetString(inputPtr + (idx - len - 1), len); - len = 0; - } - else - { - len++; - } -#endif - } while (strIdx < count); - } - } - - public static void GetLongsFromStream(this Stream reader, out long[] outputLongs, long count, long checkCount) - { - outputLongs = new long[count]; - long backValue = -1; - - for (long i = 0; i < count; i++) - { - long num = reader.ReadLong7Bit(); - backValue += 1 + num; - if (backValue > checkCount) throw new InvalidDataException($"[PatchDir::GetLongsFromStream] Given back value for the reference list is invalid! Having {i} refs while expecting max: {checkCount}"); -#if DEBUG && SHOWDEBUGINFO - HDiffPatch.Event.PushLog($"[PatchDir::GetLongsFromStream] value {i} - {count}: {backValue}", Verbosity.Debug); -#endif - outputLongs[i] = backValue; - } - } - - public static void GetLongsFromStream(this Stream reader, out long[] outputLongs, long count) - { - outputLongs = new long[count]; - for (long i = 0; i < count; i++) - { - long num = reader.ReadLong7Bit(); - outputLongs[i] = num; -#if DEBUG && SHOWDEBUGINFO - HDiffPatch.Event.PushLog($"[PatchDir::GetLongsFromStream] value {i} - {count}: {num}", Verbosity.Debug); -#endif - } - } - - public static void GetPairIndexReferenceFromStream(this Stream reader, out PairIndexReference[] outPair, long pairCount, long checkEndNewValue, long checkEndOldValue) - { - outPair = new PairIndexReference[pairCount]; - long backNewValue = -1; - long backOldValue = -1; - - for (long i = 0; i < pairCount; ++i) - { - long incNewValue = reader.ReadLong7Bit(); - - backNewValue += 1 + incNewValue; - if (backNewValue > checkEndNewValue) throw new InvalidDataException($"[PatchDir::GetArrayOfSamePairULongTag] Given back new value for the list is invalid! Having {backNewValue} value while expecting max: {checkEndNewValue}"); - - byte pSign = (byte)reader.ReadByte(); - long incOldValue = reader.ReadLong7Bit(1, pSign); - - if (pSign >> (8 - 1) == 0) - backOldValue += 1 + incOldValue; - else - backOldValue = backOldValue + 1 - incOldValue; - - if (backOldValue > checkEndOldValue) throw new InvalidDataException($"[PatchDir::GetArrayOfSamePairULongTag] Given back old value for the list is invalid! Having {backOldValue} value while expecting max: {checkEndOldValue}"); -#if DEBUG && SHOWDEBUGINFO - HDiffPatch.Event.PushLog($"[PatchDir::GetArrayOfSamePairULongTag] value {i} - {pairCount}: newIndex -> {backNewValue} oldIndex -> {backOldValue}", Verbosity.Debug); -#endif - outPair[i] = new PairIndexReference { NewIndex = backNewValue, OldIndex = backOldValue }; - } - } - - public static int GetFileStreamBufferSize(this long fileSize) - => fileSize switch - { - // 128 KiB - <= 128 << 10 => 4 << 10, - // 1 MiB - <= 1 << 20 => 64 << 10, - // 32 MiB - <= 32 << 20 => 128 << 10, - // 100 MiB - <= 100 << 20 => 512 << 10, - _ => 1 << 20 - }; -} diff --git a/SharpHDiffPatch.Core/Binary/Compression/CompressionStreamHelper.cs b/SharpHDiffPatch.Core/Binary/Compression/CompressionStreamHelper.cs deleted file mode 100644 index ff4fd0d..0000000 --- a/SharpHDiffPatch.Core/Binary/Compression/CompressionStreamHelper.cs +++ /dev/null @@ -1,140 +0,0 @@ -// ReSharper disable IdentifierTypo -// ReSharper disable ConvertSwitchStatementToSwitchExpression -// ReSharper disable CommentTypo -// ReSharper disable InconsistentNaming - -using System; -using System.IO; -using System.IO.Compression; -using System.Runtime.InteropServices; -using SharpHDiffPatch.Core.Binary.Compression.BZip2; -using SharpHDiffPatch.Core.Binary.Compression.Lzma; -using SharpHDiffPatch.Core.Binary.Streams; - -#if NET6_0_OR_GREATER -using System.Collections.Generic; -using ZstdNet; -#endif - -#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER -using ZstdManagedDecompressor = ZstdSharp.Decompressor; -using ZstdManagedDecompressorParameter = ZstdSharp.Unsafe.ZSTD_dParameter; -using ZstdManagedStream = ZstdSharp.DecompressionStream; -#endif - -#if !NETSTANDARD2_0_OR_GREATER -using ZstdNativeDecompressor = ZstdNet.DecompressionOptions; -using ZstdNativeDecompressorParameter = ZstdNet.ZSTD_dParameter; -using ZstdNativeStream = ZstdNet.DecompressionStream; -#endif - -namespace SharpHDiffPatch.Core.Binary.Compression -{ - public enum HDiffCompressionMode - { - nocomp, - zstd, - lzma, - lzma2, - zlib, - bz2, - pbz2 - } - - internal static class CompressionStreamHelper - { - private delegate Stream ZstdStreamFallback(Stream stream); - private static ZstdStreamFallback _createZstdStreamFallback; - private static readonly int ZstdWindowLogMax = Environment.Is64BitProcess ? 31 : 30; - - internal static void GetDecompressStreamPlugin( - HDiffCompressionMode type, - Stream sourceStream, - out Stream decompStream, - long length, - long compLength, - out long outLength, - bool isBuffered) - { - long toPosition = sourceStream.Position; - outLength = compLength > 0 ? compLength : length; - long toCompLength = sourceStream.Position + outLength; - - HDiffPatch.Event.PushLog($"[PatchCore::GetDecompressStreamPlugin] Assigning stream of compression: {type} at start pos: {toPosition} to end pos: {toCompLength}", Verbosity.Verbose); - Stream rawStream; - if (isBuffered) - rawStream = new ChunkStream(sourceStream, toPosition, toCompLength); - else - { - sourceStream.Position = toPosition; - rawStream = sourceStream; - } - - if (type != HDiffCompressionMode.nocomp && compLength == 0) - { - decompStream = rawStream; - return; - } - - decompStream = type switch - { - HDiffCompressionMode.nocomp => rawStream, - HDiffCompressionMode.zstd => CreateZstdStream(rawStream), - HDiffCompressionMode.zlib => new DeflateStream(rawStream, CompressionMode.Decompress, true), - HDiffCompressionMode.bz2 => new BZip2InputStream(rawStream, false, true), - HDiffCompressionMode.pbz2 => new BZip2InputStream(rawStream, true, true), - HDiffCompressionMode.lzma or HDiffCompressionMode.lzma2 => CreateLzmaStream(rawStream), - _ => throw new NotSupportedException($"[PatchCore::GetDecompressStreamPlugin] Compression Type: {type} is not supported") - }; - } - - private static Stream CreateZstdStream(Stream rawStream) - { - if (_createZstdStreamFallback != null) return _createZstdStreamFallback(rawStream); - -#if !(NETSTANDARD2_0_OR_GREATER || NET461_OR_GREATER) - if (DllUtils.IsLibraryExist(DllUtils.DllName)) - _createZstdStreamFallback = CreateZstdNativeStream; - else - _createZstdStreamFallback = CreateZstdManagedStream; -#else - _createZstdStreamFallback = CreateZstdManagedStream; -#endif - return _createZstdStreamFallback(rawStream); - } - - /* HACK: The default window log max size is 30. This is unacceptable since the native HPatch implementation - * always use 31 as the size_t, which is 8 bytes length. - * - * Code Snippets (decompress_plugin_demo.h:963): - * #define _ZSTD_WINDOWLOG_MAX ((sizeof(size_t)<=4)?30:31) - */ -#if !NETSTANDARD2_0_OR_GREATER - private static Stream CreateZstdNativeStream(Stream rawStream) => - new ZstdNativeStream(rawStream, new ZstdNativeDecompressor(null, new Dictionary - { - { ZstdNativeDecompressorParameter.ZSTD_d_windowLogMax, ZstdWindowLogMax } - })); -#endif - - private static Stream CreateZstdManagedStream(Stream rawStream) - { - ZstdManagedDecompressor decompressor = new(); - decompressor.SetParameter(ZstdManagedDecompressorParameter.ZSTD_d_windowLogMax, ZstdWindowLogMax); - return new ZstdManagedStream(rawStream, decompressor, 16 << 10); - } - - private static Stream CreateLzmaStream(Stream rawStream) - { - int propLen = rawStream.ReadByte(); - if (propLen != 5) return new LzmaInputStream([(byte)propLen], rawStream, true); // Get LZMA2 if propLen != 5 - - // Get LZMA if propLen == 5 - byte[] props = new byte[propLen]; - _ = rawStream.Read(props, 0, propLen); - int dicSize = MemoryMarshal.Read(props.AsSpan(1)); - HDiffPatch.Event.PushLog($"[PatchCore::CreateLzmaStream] Assigning LZMA stream with dictionary size: {dicSize}", Verbosity.Verbose); - return new LzmaInputStream(props, rawStream, -1, -1, rawStream, false, true); - } - } -} diff --git a/SharpHDiffPatch.Core/Binary/Streams/ChunkStream.cs b/SharpHDiffPatch.Core/Binary/Streams/ChunkStream.cs deleted file mode 100644 index 6c82271..0000000 --- a/SharpHDiffPatch.Core/Binary/Streams/ChunkStream.cs +++ /dev/null @@ -1,176 +0,0 @@ -using System; -using System.Buffers; -using System.IO; - -namespace SharpHDiffPatch.Core.Binary.Streams; - -public sealed class ChunkStream : Stream -{ - private readonly Stream _stream; - private long Start { get; } - private long End { get; } - private long Size => End - Start; - private long CurPos { get; set; } - private long Remain => Size - CurPos; - private bool IsDisposing { get; } - - public ChunkStream(Stream stream, long start, long end, bool isDisposing = false) - { - _stream = stream; - - if (_stream.Length == 0) - { - throw new Exception("The stream must not have 0 bytes!"); - } - - if (_stream.Length < start || end > _stream.Length) - { - throw new ArgumentOutOfRangeException(nameof(stream), "Offset is out of stream size range!"); - } - - _stream.Position = start; - Start = start; - End = end; - CurPos = 0; - IsDisposing = isDisposing; - } - - ~ChunkStream() => Dispose(IsDisposing); - -#if !(NETSTANDARD2_0 || NET461_OR_GREATER) - public override int Read(Span buffer) - { - if (Remain == 0) return 0; - - int toSlice = (int)(buffer.Length > Remain ? Remain : buffer.Length); - _stream.Position = Start + CurPos; - int read = _stream.Read(buffer[..toSlice]); - CurPos += read; - - return read; - } - - public override void Write(ReadOnlySpan buffer) - { - if (Remain == 0) return; - - int toSlice = (int)(buffer.Length > Remain ? Remain : buffer.Length); - CurPos += toSlice; - - _stream.Write(buffer[..toSlice]); - } -#endif - - public override int Read(byte[] buffer, int offset, int count) - { - if (Remain == 0) return 0; - - int toRead = (int)(Remain < count ? Remain : count); - _stream.Position = Start + CurPos; - int read = _stream.Read(buffer, offset, toRead); - CurPos += read; - return read; - } - - public override void Write(byte[] buffer, int offset, int count) - { - int toRead = (int)(Remain < count ? Remain : count); - int toOffset = offset > Remain ? 0 : offset; - _stream.Position += toOffset; - CurPos += toOffset + toRead; - - _stream.Write(buffer, offset, toRead); - } - -#if !(NETSTANDARD2_0 || NET461_OR_GREATER) - public override void CopyTo(Stream destination, int bufferSize) - { - if (bufferSize <= 0) bufferSize = 4 << 10; - - byte[] buffer = ArrayPool.Shared.Rent(bufferSize); - try - { - int read; - while ((read = Read(buffer.AsSpan(0, bufferSize))) > 0) - { - destination.Write(buffer.AsSpan(0, read)); - } - } - finally - { - ArrayPool.Shared.Return(buffer); - } - } -#endif - - public override bool CanRead => _stream.CanRead; - - public override bool CanSeek => _stream.CanSeek; - - public override bool CanWrite => _stream.CanWrite; - - public override void Flush() - { - _stream.Flush(); - } - - public override long Length => Size; - - public override long Position - { - get => CurPos; - set - { - if (value > Size) - { - throw new IndexOutOfRangeException(); - } - - CurPos = value; - _stream.Position = CurPos + Start; - } - } - - public override long Seek(long offset, SeekOrigin origin) - { - switch (origin) - { - case SeekOrigin.Begin: - { - if (offset > Size) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } - return _stream.Seek(offset + Start, SeekOrigin.Begin) - Start; - } - case SeekOrigin.Current: - { - long pos = _stream.Position - Start; - if (pos + offset > Size) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } - return _stream.Seek(offset, SeekOrigin.Current) - Start; - } - case SeekOrigin.End: - default: - { - _stream.Position = End; - _stream.Position -= offset; - - return Position; - } - } - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - protected override void Dispose(bool disposing) - { - if (disposing) base.Dispose(true); - if (IsDisposing) _stream.Dispose(); - } -} \ No newline at end of file diff --git a/SharpHDiffPatch.Core/Binary/Streams/CombinedStream.cs b/SharpHDiffPatch.Core/Binary/Streams/CombinedStream.cs deleted file mode 100644 index 7fc1e95..0000000 --- a/SharpHDiffPatch.Core/Binary/Streams/CombinedStream.cs +++ /dev/null @@ -1,672 +0,0 @@ -// ReSharper disable CommentTypo - -/* - * Original Code by lassevk - * https://raw.githubusercontent.com/lassevk/Streams/master/Streams/CombinedStream.cs - */ - -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace SharpHDiffPatch.Core.Binary.Streams; - -public class CombinedStreamSegment - where T : Stream -{ - public T Stream { get; set; } - public long Length { get; set; } -} - -/// -/// This class is a descendant that manages multiple underlying -/// streams which are considered to be chained together to one large stream. Only reading -/// and seeking is allowed, writing will throw exceptions. -/// -public sealed class CombinedStream : Stream - where T : Stream -{ - private readonly T[] _underlyingStreams; - private readonly long[] _streamEnds; - - private readonly bool _leaveOpen; - private readonly bool _canWrite; - - private long _position; - private int _index; - private bool _disposed; - - /// - /// Constructs a new on top of the specified array - /// of streams. - /// - /// - /// An array of objects that will be chained together and - /// considered to be one big stream. - /// - /// Keep all underlying streams opened while disposing this current stream. - public CombinedStream(T[] underlyingStreams, - bool leaveOpen = false) - { - if (underlyingStreams == null) - throw new ArgumentNullException(nameof(underlyingStreams), $"[{nameof(CombinedStream)}()] underlyingStreams"); - - if (underlyingStreams.Length == 0) - { - throw new ArgumentException($"[{nameof(CombinedStream)}()] At least one stream is required.", - nameof(underlyingStreams)); - } - - _underlyingStreams = underlyingStreams; - _streamEnds = new long[underlyingStreams.Length]; - _leaveOpen = leaveOpen; - - bool canWrite = true; - long totalLength = 0; - - for (int i = 0; i < _underlyingStreams.Length; i++) - { - Stream stream = _underlyingStreams[i]; - - if (stream == null) - { - throw new ArgumentException($"[{nameof(CombinedStream)}()] The array contains a null stream.", - nameof(underlyingStreams)); - } - - if (!stream.CanRead) - { - throw new ArgumentException($"[{nameof(CombinedStream)}()] Every underlying stream must be readable.", - nameof(underlyingStreams)); - } - - if (!stream.CanSeek) - { - throw new ArgumentException($"[{nameof(CombinedStream)}()] Every underlying stream must be seekable.", - nameof(underlyingStreams)); - } - - canWrite &= stream.CanWrite; - - totalLength = checked(totalLength + stream.Length); - _streamEnds[i] = totalLength; - } - - Length = totalLength; - _canWrite = canWrite; - -#if SHOWMOREDEBUGINFO - HDiffPatch.Event.PushLog($"[{nameof(CombinedStream)}()] Total length of the CombinedStream: {totalLength} bytes with total of {underlyingStreams.Length} streams", Verbosity.Debug); -#endif - } - - /// - /// Constructs a new on top of the specified array - /// of streams. - /// - /// - /// An array of objects that will be chained together and - /// considered to be one big stream. - /// - /// Keep all underlying streams opened while disposing this current stream. - public CombinedStream(CombinedStreamSegment[] underlyingStreams, - bool leaveOpen = false) - { - if (underlyingStreams == null) - throw new ArgumentNullException(nameof(underlyingStreams), $"[{nameof(CombinedStream)}()] underlyingStreams"); - - if (underlyingStreams.Length == 0) - { - throw new ArgumentException($"[{nameof(CombinedStream)}()] At least one stream is required.", - nameof(underlyingStreams)); - } - - _underlyingStreams = new T[underlyingStreams.Length]; - _streamEnds = new long[underlyingStreams.Length]; - _leaveOpen = leaveOpen; - - bool canWrite = true; - long totalLength = 0; - - for (int i = 0; i < underlyingStreams.Length; i++) - { - CombinedStreamSegment segment = underlyingStreams[i]; - - if (segment == null) - { - throw new ArgumentException($"[{nameof(CombinedStream)}()] The array contains a null segment.", nameof(underlyingStreams)); - } - - T stream = segment.Stream; - - if (stream == null) - { - throw new ArgumentException($"[{nameof(CombinedStream)}()] A segment contains a null stream.", nameof(underlyingStreams)); - } - - if (segment.Length < 0) - { - throw new ArgumentOutOfRangeException(nameof(underlyingStreams), $"[{nameof(CombinedStream)}()] Segment length cannot be negative."); - } - - if (!stream.CanRead) - { - throw new ArgumentException($"[{nameof(CombinedStream)}()] Every underlying stream must be readable.", nameof(underlyingStreams)); - } - - if (!stream.CanSeek) - { - throw new ArgumentException($"[{nameof(CombinedStream)}()] Every underlying stream must be seekable.", nameof(underlyingStreams)); - } - - canWrite &= stream.CanWrite; - - _underlyingStreams[i] = stream; - - totalLength = checked(totalLength + segment.Length); - _streamEnds[i] = totalLength; - } - - Length = totalLength; - _canWrite = canWrite; - -#if SHOWMOREDEBUGINFO - HDiffPatch.Event.PushLog($"[{nameof(CombinedStream)}()] Total length of the CombinedStream: {totalLength} bytes with total of {underlyingStreams.Length} streams", Verbosity.Debug); -#endif - } - - /// - public override bool CanRead => !_disposed; - - /// - public override bool CanSeek => !_disposed; - - /// - public override bool CanWrite => !_disposed && _canWrite; - - /// - public override void Flush() - { - foreach (T stream in _underlyingStreams) - stream.Flush(); - } - - /// - public override async Task FlushAsync(CancellationToken cancellationToken) - { - ThrowIfDisposed(); - - if (!_canWrite) - return; - - foreach (T stream in _underlyingStreams) - await stream.FlushAsync(cancellationToken).ConfigureAwait(false); - } - - /// - protected override void Dispose(bool disposing) - { - if (!_disposed) - { - _disposed = true; - - if (disposing && !_leaveOpen) - { - foreach (T stream in _underlyingStreams) - stream.Dispose(); - } - } - - base.Dispose(disposing); - } - - /// - public override long Length { get; } - - /// - public override long Position - { - get - { - ThrowIfDisposed(); - return _position; - } - set - { - ThrowIfDisposed(); - - if ((ulong)value > (ulong)Length) - throw new ArgumentOutOfRangeException(nameof(value)); - - _position = value; - _index = FindStreamIndex(_streamEnds, value, Length, _index); - } - } - - /// - public override void SetLength(long value) => throw new NotSupportedException($"[{nameof(CombinedStream)}::SetLength] The method or operation is not supported by CombinedStream."); - -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER - /// - public override int Read(Span buffer) - { - ThrowIfDisposed(); - - if (buffer.IsEmpty || _position == Length) - return 0; - - int totalRead = 0; - - while (!buffer.IsEmpty && _position < Length) - { - SkipExhaustedStreams(); - - Stream stream = _underlyingStreams[_index]; - long streamStart = GetStreamStart(_streamEnds, _index); - long localPosition = _position - streamStart; - long available = _streamEnds[_index] - _position; - - if (available <= 0) - { - if (!MoveNextStream()) - break; - - continue; - } - - int requested = (int)Math.Min(buffer.Length, available); - - int read; -#if NET6_0_OR_GREATER - if (stream is FileStream { SafeFileHandle: not null } fileStream) - { - read = RandomAccess.Read(fileStream.SafeFileHandle, - buffer[..requested], - localPosition); - goto Advance; - } -#endif - - if (stream.Position != localPosition) - stream.Position = localPosition; - - read = stream.Read(buffer[..requested]); - - if (read == 0) - { - if (_position < _streamEnds[_index]) - { - throw new EndOfStreamException($"[{nameof(CombinedStream)}::ReadCore] An underlying stream ended before its expected length."); - } - - if (!MoveNextStream()) - break; - - continue; - } - - Advance: - totalRead += read; - _position += read; - buffer = buffer[read..]; - } - - return totalRead; - } -#endif - - /// - public override int Read(byte[] buffer, int offset, int count) - { - ValidateBufferArguments(buffer, offset, count); - ThrowIfDisposed(); - - return ReadCore(buffer, offset, count); - } - - private int ReadCore(byte[] buffer, int offset, int count) - { - if (count == 0 || _position == Length) - return 0; - - int totalRead = 0; - - while (count != 0 && _position < Length) - { - SkipExhaustedStreams(); - - Stream stream = _underlyingStreams[_index]; - long streamStart = GetStreamStart(_streamEnds, _index); - long localPosition = _position - streamStart; - long available = _streamEnds[_index] - _position; - - if (available <= 0) - { - if (!MoveNextStream()) - break; - - continue; - } - - int requested = (int)Math.Min(count, available); - - int read; -#if NET6_0_OR_GREATER - if (stream is FileStream { SafeFileHandle: not null } fileStream) - { - read = RandomAccess.Read(fileStream.SafeFileHandle, - buffer.AsSpan(offset, requested), - localPosition); - goto Advance; - } -#endif - if (stream.Position != localPosition) - stream.Position = localPosition; - - read = stream.Read(buffer, offset, requested); - - if (read == 0) - { - // The FileStream became shorter than the length captured - // by this CombinedStream, or another owner changed it. - if (_position < _streamEnds[_index]) - { - throw new EndOfStreamException($"[{nameof(CombinedStream)}::ReadCore] An underlying stream ended before its expected length."); - } - - if (!MoveNextStream()) - break; - - continue; - } - - Advance: - totalRead += read; - offset += read; - count -= read; - _position += read; - } - - return totalRead; - } - - /// - public override long Seek(long offset, SeekOrigin origin) - { - ThrowIfDisposed(); - long position = origin switch - { - SeekOrigin.Begin => offset, - SeekOrigin.Current => checked(_position + offset), - SeekOrigin.End => checked(Length + offset), - _ => throw new ArgumentOutOfRangeException(nameof(origin)) - }; - - Position = position; - return position; - } - -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER - /// - public override void Write(ReadOnlySpan buffer) - { - ThrowIfDisposed(); - EnsureWritable(buffer.Length); - - while (!buffer.IsEmpty) - { - SkipExhaustedStreams(); - - Stream stream = _underlyingStreams[_index]; - long streamStart = GetStreamStart(_streamEnds, _index); - long localPosition = _position - streamStart; - long available = _streamEnds[_index] - _position; - - if (available <= 0) - { - if (!MoveNextStream()) - { - throw new EndOfStreamException($"[{nameof(CombinedStream)}::Write] The write exceeds the combined stream length."); - } - - continue; - } - - int writable = (int)Math.Min(buffer.Length, available); - -#if NET6_0_OR_GREATER - if (stream is FileStream { SafeFileHandle: not null } fileStream) - { - RandomAccess.Write(fileStream.SafeFileHandle, - buffer[..writable], - localPosition); - goto Advance; - } -#endif - - if (stream.Position != localPosition) - stream.Position = localPosition; - - stream.Write(buffer[..writable]); - - Advance: - _position += writable; - buffer = buffer[writable..]; - } - } -#endif - - /// - public override void Write(byte[] buffer, int offset, int count) - { - ValidateBufferArguments(buffer, offset, count); - ThrowIfDisposed(); - EnsureWritable(count); - - WriteCore(buffer, offset, count); - } - - private void WriteCore(byte[] buffer, int offset, int count) - { - while (count != 0) - { - SkipExhaustedStreams(); - - Stream stream = _underlyingStreams[_index]; - long streamStart = GetStreamStart(_streamEnds, _index); - long localPosition = _position - streamStart; - long available = _streamEnds[_index] - _position; - - if (available <= 0) - { - if (!MoveNextStream()) - { - throw new EndOfStreamException($"[{nameof(CombinedStream)}::WriteCore] The write exceeds the combined stream length."); - } - - continue; - } - - int writable = (int)Math.Min(count, available); - -#if NET6_0_OR_GREATER - if (stream is FileStream { SafeFileHandle: not null } fileStream) - { - RandomAccess.Write(fileStream.SafeFileHandle, - buffer.AsSpan(offset, writable), - localPosition); - goto Advance; - } -#endif - - if (stream.Position != localPosition) - stream.Position = localPosition; - - stream.Write(buffer, offset, writable); - - Advance: - offset += writable; - count -= writable; - _position += writable; - } - } - - private void ThrowIfDisposed() - { - if (_disposed) - throw new ObjectDisposedException(nameof(CombinedStream)); - } - -#if !NETSTANDARD2_1_OR_GREATER && !NETCOREAPP2_1_OR_GREATER - private static void ValidateBufferArguments( - byte[] buffer, - int offset, - int count) - { - if (buffer == null) - throw new ArgumentNullException(nameof(buffer)); - - if (offset < 0) - throw new ArgumentOutOfRangeException(nameof(offset)); - - if (count < 0) - throw new ArgumentOutOfRangeException(nameof(count)); - - if (buffer.Length - offset < count) - throw new ArgumentException($"[{nameof(CombinedStream)}::ValidateBufferArguments] Offset and count exceed the buffer length."); - } -#endif - - private void SkipExhaustedStreams() - { - while (_index < _underlyingStreams.Length - 1 && - _position >= _streamEnds[_index]) - { - _index++; - } - } - - private bool MoveNextStream() - { - if (_index >= _underlyingStreams.Length - 1) - return false; - - _index++; - return true; - } - - private void EnsureWritable(int count) - { - if (!_canWrite) - throw new NotSupportedException($"[{nameof(CombinedStream)}::EnsureWritable] The stream is not writable."); - - if (count > Length - _position) - { - throw new EndOfStreamException($"[{nameof(CombinedStream)}::EnsureWritable] The write exceeds the combined stream's fixed length."); - } - } - - private static long GetStreamStart(long[] streamEnds, int index) => index == 0 ? 0 : streamEnds[index - 1]; - - private static int FindStreamIndex(long[] streamEnds, long position, long length, int index) - { - int lastIndex = streamEnds.Length - 1; - - // Position == Length represents EOF. Keep the final stream selected, - // including when it is a zero-length trailing stream. - if (position == length) - return lastIndex; - - long currentStart = index == 0 ? 0 : streamEnds[index - 1]; - long currentEnd = streamEnds[index]; - - // Most common case: the new position remains in the current segment. - if (position >= currentStart && position < currentEnd) - return index; - - return position >= currentEnd - ? FindForward(streamEnds, position, index, lastIndex) - : FindBackward(streamEnds, position, index); - } - - private static int FindForward(long[] streamEnds, long position, int currentIndex, int lastIndex) - { - int nextIndex = currentIndex + 1; - - // Common when reading or seeking across one boundary. - if (nextIndex <= lastIndex && - position < streamEnds[nextIndex]) - { - return nextIndex; - } - - int low = nextIndex; - int high = nextIndex; - int step = 1; - - // Find an upper bound exponentially rather than searching the - // entire remaining range immediately. - while (high < lastIndex && position >= streamEnds[high]) - { - low = high + 1; - - int remaining = lastIndex - high; - int increment = step < remaining ? step : remaining; - - high += increment; - - if (step <= int.MaxValue / 2) - step <<= 1; - } - - return FindFirstEndGreaterThan(streamEnds, position, low, high); - } - - private static int FindBackward(long[] streamEnds, long position, int currentIndex) - { - int high = currentIndex - 1; - - // Adjacent segment fast path. - if (high >= 0) - { - long start = high == 0 ? 0 : streamEnds[high - 1]; - - if (position >= start && position < streamEnds[high]) - return high; - } - - int low = high; - int step = 1; - - while (low > 0 && position < streamEnds[low - 1]) - { - int decrement = Math.Min(step, low); - low -= decrement; - - if (step <= int.MaxValue / 2) - step <<= 1; - } - - return FindFirstEndGreaterThan(streamEnds, position, low, high); - } - - private static int FindFirstEndGreaterThan( - long[] streamEnds, - long position, - int low, - int high) - { - while (low < high) - { - int middle = low + ((high - low) >> 1); - - if (streamEnds[middle] > position) - high = middle; - else - low = middle + 1; - } - - return low; - } -} \ No newline at end of file diff --git a/SharpHDiffPatch.Core/Event/PatchEvent.cs b/SharpHDiffPatch.Core/Event/PatchEvent.cs deleted file mode 100644 index 325d583..0000000 --- a/SharpHDiffPatch.Core/Event/PatchEvent.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Threading; - -namespace SharpHDiffPatch.Core.Event -{ - public class LoggerEvent(string message, Verbosity logLevel) - { - public Verbosity LogLevel = logLevel; - public string Message = message; - } - - public sealed class PatchEvent - { - private const double ScOneSecond = 1000; - private long _scLastTick = Environment.TickCount; - private long _scLastReceivedBytes; - private double _scLastSpeed; - - public void UpdateEvent(long currentSizePatched, long totalSizeToBePatched, long read, double totalSecond) - { - Speed = (long)(currentSizePatched / totalSecond); - CurrentSizePatched = currentSizePatched; - TotalSizeToBePatched = totalSizeToBePatched; - Read = read; - } - - private long _currentSizePatched; - public long CurrentSizePatched - { - get => _currentSizePatched; - private set - { - double speed = CalculateSpeed(value - _currentSizePatched); - Speed = (long)speed; - TimeLeft = TimeSpan.FromSeconds((TotalSizeToBePatched - value) / (double.IsInfinity(speed) || speed <= 0 ? 1 : speed)); - ProgressPercentage = Math.Round(value / (double)TotalSizeToBePatched * 100, 2); - _currentSizePatched = value; - } - } - - public long TotalSizeToBePatched { get; private set; } - public double ProgressPercentage { get; private set; } - public long Read { get; private set; } - public long Speed { get; private set; } - public TimeSpan TimeLeft { get; private set; } - - private double CalculateSpeed(long receivedBytes) => CalculateSpeed(receivedBytes, ref _scLastSpeed, ref _scLastReceivedBytes, ref _scLastTick); - - private static double CalculateSpeed(long receivedBytes, ref double lastSpeedToUse, ref long lastReceivedBytesToUse, ref long lastTickToUse) - { - long currentTick = Environment.TickCount - lastTickToUse + 1; - long totalReceivedInSecond = Interlocked.Add(ref lastReceivedBytesToUse, receivedBytes); - double speed = totalReceivedInSecond * ScOneSecond / currentTick; - - if (!(currentTick > ScOneSecond)) - { - return lastSpeedToUse; - } - - lastSpeedToUse = speed; - _ = Interlocked.Exchange(ref lastSpeedToUse, speed); - _ = Interlocked.Exchange(ref lastReceivedBytesToUse, 0); - _ = Interlocked.Exchange(ref lastTickToUse, Environment.TickCount); - return lastSpeedToUse; - } - } -} diff --git a/SharpHDiffPatch.Core/Extern.cs b/SharpHDiffPatch.Core/Extern.cs deleted file mode 100644 index 22ccd9e..0000000 --- a/SharpHDiffPatch.Core/Extern.cs +++ /dev/null @@ -1,58 +0,0 @@ -#if NET6_0_OR_GREATER -using System; -using System.IO; -using System.Reflection; -using System.Runtime.InteropServices; - -namespace SharpHDiffPatch.Core; - -internal class Extern -{ - private static readonly string CurrentProcPath = Environment.ProcessPath?.TrimEnd(Path.DirectorySeparatorChar); - private static readonly string LibArchitecturePrefix = GetLibArchitecturePrefix(); - private static readonly string LibPlatformNamePrefix = GetLibPlatformNamePrefix(); - private static readonly string LibExtensionPrefix = GetLibExtensionPrefix(); - private static readonly string LibFolderPath = Path.Combine("Lib", LibPlatformNamePrefix); - private static readonly string LibFullPath = Path.Combine(CurrentProcPath, LibFolderPath, "{0}" + LibExtensionPrefix); - - static Extern() - { - // Use custom Dll import resolver - NativeLibrary.SetDllImportResolver(Assembly.GetExecutingAssembly(), DllImportResolver); - } - - private static string GetLibPlatformNamePrefix() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - return $"win-{LibArchitecturePrefix}"; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - return $"linux-{LibArchitecturePrefix}"; - return RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "osx" : "unknown"; - } - - private static string GetLibExtensionPrefix() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - return ".dll"; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - return ".so"; - return RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? ".dylib" : string.Empty; - } - - private static string GetLibArchitecturePrefix() => RuntimeInformation.OSArchitecture.ToString().ToLower(); - - private static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) - { - libraryName = string.Format(LibFullPath, libraryName); - string searchPathName = searchPath == null ? "Default" : searchPath.ToString(); - HDiffPatch.Event.PushLog($"[Extern::DllImportResolver] Loading library from path: {libraryName} | Search path: {searchPathName}", Verbosity.Debug); - // Try load the library and if fails, then throw. - bool isLoadSuccessful = NativeLibrary.TryLoad(libraryName, assembly, searchPath, out IntPtr pResult); - if (!isLoadSuccessful || pResult == IntPtr.Zero) - throw new FileLoadException($"Failed while loading library from this path: {libraryName}\r\nMake sure that the library/.dll is exist or valid and not corrupted!"); - - // If success, then return the pointer to the library - return pResult; - } -} -#endif \ No newline at end of file diff --git a/SharpHDiffPatch.Core/HDiffPatch.cs b/SharpHDiffPatch.Core/HDiffPatch.cs deleted file mode 100644 index ab2afcd..0000000 --- a/SharpHDiffPatch.Core/HDiffPatch.cs +++ /dev/null @@ -1,243 +0,0 @@ -using SharpHDiffPatch.Core.Event; -using SharpHDiffPatch.Core.Patch; -using System; -using System.Diagnostics; -using System.IO; -using System.Threading; -using SharpHDiffPatch.Core.Binary.Compression; - -namespace SharpHDiffPatch.Core; - -public enum BufferMode { None, Partial, Full } -public enum Verbosity { Quiet, Info, Verbose, Debug } -public enum DirectoryPatchFormat { Standard, Kuro } - -public enum ChecksumMode -{ - NoChecksum, - Crc32, - FAdler64 -} - -public struct HeaderInfoExt -{ - public HeaderInfo HeaderInfo; - public DataReferenceInfo DataReferenceInfo; -} - -public struct HeaderInfo -{ - public HDiffCompressionMode CompMode; - public ChecksumMode ChecksumMode; - - public bool IsInputDir; - public bool IsOutputDir; - public bool IsSingleCompressedDiff; - public string PatchPath; - public Func PatchCreateStream; - public string HeaderMagic; - - public long StepMemSize; - - public bool DirDataIsCompressed; - - public long OldDataSize; - public long NewDataSize; - public long CompressedCount; - - public DiffSingleChunkInfo SingleChunkInfo; - public DiffChunkInfo ChunkInfo; -} - -public struct DataReferenceInfo -{ - public long InputDirCount; - public long InputRefFileCount; - public long InputRefFileSize; - public long InputSumSize; - - public long OutputDirCount; - public long OutputRefFileCount; - public long OutputRefFileSize; - public long OutputSumSize; - - public long SameFilePairCount; - public long SameFileSize; - - public int NewExecuteCount; - - public long PrivateReservedDataSize; - public long PrivateExternDataSize; - public long PrivateExternDataOffset; - - public long ExternDataOffset; - public long ExternDataSize; - - public long CompressSizeBeginPos; - - public byte ChecksumByteSize; - public long ChecksumOffset; - - public long HeadDataSize; - public long HeadDataOffset; - public long HeadDataCompressedSize; - - public long HDiffDataOffset; - public long HDiffDataSize; -} - -public struct DiffSingleChunkInfo -{ - public long UncompressedSize; - public long CompressedSize; - - public long DiffDataPos; -} - -public struct DiffChunkInfo -{ - public long TypesEndPos; - public long CoverCount; - public long CompressSizeBeginPos; - public long CoverBufSize; - public long CompressCoverBufSize; - public long RleCtrlBufSize; - public long CompressRleCtrlBufSize; - public long RleCodeBufSize; - public long CompressRleCodeBufSize; - public long NewDataDiffSize; - public long CompressNewDataDiffSize; - public long HeadEndPos; - public long CoverEndPos; -} - -public sealed class HDiffPatch -{ - private HeaderInfo _headerInfo; - private DataReferenceInfo ReferenceInfo { get; set; } - private Stream DiffStream { get; set; } - private bool IsPatchDir { get; set; } = true; - - internal static PatchEvent PatchEvent = new(); - public static EventListener Event = new(); - - public static Verbosity LogVerbosity { get; set; } = Verbosity.Quiet; - public DirectoryPatchFormat DirPatchFormat { get; set; } = DirectoryPatchFormat.Standard; - -#region Header Initialization - public void Initialize(string diff) - { - using (DiffStream = new FileStream(diff, FileMode.Open, FileAccess.Read)) - { - IsPatchDir = Header.TryParseHeaderInfo(DiffStream, diff, out HeaderInfo info, out DataReferenceInfo reference); - _headerInfo = info; - ReferenceInfo = reference; - } - } - - public void Initialize(Func diffCreateStream) - { - using (DiffStream = diffCreateStream()) - { - IsPatchDir = Header.TryParseHeaderInfo(DiffStream, null, out HeaderInfo info, out DataReferenceInfo reference); - _headerInfo = info; - ReferenceInfo = reference; - _headerInfo.PatchCreateStream = diffCreateStream; - } - } - - public void Patch(string inputPath, string outputPath, bool useBufferedPatch, CancellationToken token = default, bool useFullBuffer = false, bool useFastBuffer = false) - => Patch(inputPath, outputPath, useBufferedPatch, null, token, useFullBuffer, useFastBuffer); - - public void Patch(string inputPath, string outputPath, bool useBufferedPatch, Action writeBytesDelegate, CancellationToken token = default, bool useFullBuffer = false, bool useFastBuffer = false) - { - IPatch patcher; - if (IsPatchDir && _headerInfo is { IsInputDir: true, IsOutputDir: true }) - { - patcher = new PatchDir(_headerInfo, ReferenceInfo, _headerInfo.PatchPath, DirPatchFormat, token); - } - else - { - patcher = new PatchSingle(_headerInfo, token); - } - patcher.Patch(inputPath, outputPath, writeBytesDelegate, useBufferedPatch, useFullBuffer, useFastBuffer); - } -#endregion - - internal static void DisplayDirPatchInformation(long oldFileSize, long newFileSize, HeaderInfo headerInfo) - { - Event.PushLog("Patch Information:"); - Event.PushLog($" Size -> Old: {oldFileSize} bytes | New: {newFileSize} bytes"); - Event.PushLog("Technical Information:"); - if (!headerInfo.IsSingleCompressedDiff) - { - Event.PushLog($" Cover Data -> Count: {headerInfo.ChunkInfo.CoverCount} | Offset: {headerInfo.ChunkInfo.HeadEndPos} | Size: {headerInfo.ChunkInfo.CoverBufSize}"); - Event.PushLog($" RLE Data -> Offset: {headerInfo.ChunkInfo.CoverEndPos} | Control: {headerInfo.ChunkInfo.RleCtrlBufSize} | Code: {headerInfo.ChunkInfo.RleCodeBufSize}"); - Event.PushLog($" Diff Data -> Size: {headerInfo.ChunkInfo.NewDataDiffSize}"); - } - else - { - Event.PushLog($" Cover Data -> Count: {headerInfo.ChunkInfo.CoverCount} | DiffDataPos: {headerInfo.SingleChunkInfo.DiffDataPos}"); - Event.PushLog($" RLE Data -> Compressed Size: {headerInfo.SingleChunkInfo.CompressedSize} | Size: {headerInfo.SingleChunkInfo.UncompressedSize}"); - } - } - - internal static void UpdateEvent(long read, ref long currentSizePatched, ref long totalSizePatched, Stopwatch patchStopwatch) - { - lock (PatchEvent) - { - PatchEvent.UpdateEvent(currentSizePatched += read, totalSizePatched, read, patchStopwatch.Elapsed.TotalSeconds); - Event.PushEvent(PatchEvent); - } - } - - public static long GetHDiffNewSize(string diffFilePath) - { - HeaderInfoExt headerInfo = GetHDiffHeaderInfo(diffFilePath); - return headerInfo.HeaderInfo.NewDataSize; - } - - public static long GetHDiffOldSize(string diffFilePath) - { - HeaderInfoExt headerInfo = GetHDiffHeaderInfo(diffFilePath); - return headerInfo.HeaderInfo.OldDataSize; - } - - public static long GetHDiffNewSize(Stream diffStream) - { - HeaderInfoExt headerInfo = GetHDiffHeaderInfo(diffStream); - return headerInfo.HeaderInfo.NewDataSize; - } - - public static long GetHDiffOldSize(Stream diffStream) - { - HeaderInfoExt headerInfo = GetHDiffHeaderInfo(diffStream); - return headerInfo.HeaderInfo.OldDataSize; - } - - public static HeaderInfoExt GetHDiffHeaderInfo(string diffFilePath) - { - using FileStream fs = new(diffFilePath, FileMode.Open, FileAccess.Read); - _ = Header.TryParseHeaderInfo(fs, diffFilePath, out HeaderInfo headerInfo, out DataReferenceInfo headerInfoReference); - return new HeaderInfoExt { HeaderInfo = headerInfo, DataReferenceInfo = headerInfoReference }; - } - - public static HeaderInfoExt GetHDiffHeaderInfo(Stream diffStream) - { - _ = Header.TryParseHeaderInfo(diffStream, null, out HeaderInfo headerInfo, out DataReferenceInfo headerInfoReference); - return new HeaderInfoExt { HeaderInfo = headerInfo, DataReferenceInfo = headerInfoReference }; - } -} - -public class EventListener -{ - // Log for external listener - public static event EventHandler PatchEvent; - public static event EventHandler LoggerEvent; - public void PushEvent(PatchEvent patchEvent) => PatchEvent?.Invoke(this, patchEvent); - public void PushLog(in string message, Verbosity logLevel = Verbosity.Info) - { - if (logLevel != Verbosity.Quiet) - LoggerEvent?.Invoke(this, new LoggerEvent(message, logLevel)); - } -} diff --git a/SharpHDiffPatch.Core/Patch/Header.cs b/SharpHDiffPatch.Core/Patch/Header.cs deleted file mode 100644 index ec31044..0000000 --- a/SharpHDiffPatch.Core/Patch/Header.cs +++ /dev/null @@ -1,273 +0,0 @@ -using SharpHDiffPatch.Core.Binary; -using System; -using System.IO; - -namespace SharpHDiffPatch.Core.Patch -{ - internal sealed class Header - { -#if !(NETSTANDARD2_0 || NET461_OR_GREATER) - private static readonly char[] HdiffHead = ['H', 'D', 'I', 'F', 'F']; -#else - private const string HdiffHead = "HDIFF"; -#endif - - internal static bool TryParseHeaderInfo(Stream sr, string diffPath, - out HeaderInfo headerInfo, out DataReferenceInfo referenceInfo) - { - headerInfo = new HeaderInfo(); - referenceInfo = new DataReferenceInfo(); - - string headerInfoLine = sr.ReadStringToNull(); - bool isPatchDir = true; - HDiffPatch.Event.PushLog($"[Header::TryParseHeaderInfo] Signature info: {headerInfoLine}", Verbosity.Debug); - - if (headerInfoLine.Length > 64 || !headerInfoLine -#if !(NETSTANDARD2_0 || NET461_OR_GREATER) - .AsSpan() -#endif - .StartsWith(HdiffHead) - ) throw new FormatException("[Header::TryParseHeaderInfo] This is not a HDiff file format!"); - - string[] hInfoArr = headerInfoLine.Split('&'); - if (hInfoArr.Length == 2) - { - byte pFileVer = TryGetVersion(hInfoArr[0]); - if (pFileVer != 13) throw new FormatException("[Header::TryParseHeaderInfo] HDiff version is unsupported. This patcher only supports the single patch file with version: 13 only!"); - - isPatchDir = false; - - headerInfo.HeaderMagic = hInfoArr[0]; - - Enum.TryParse(hInfoArr[1], true, out headerInfo.CompMode); - HDiffPatch.Event.PushLog($"[Header::TryParseHeaderInfo] Version: {pFileVer} Compression: {headerInfo.CompMode}", Verbosity.Debug); - } - else if (hInfoArr.Length != 3) throw new IndexOutOfRangeException($"[Header::TryParseHeaderInfo] Header info is incomplete! Expecting 3 parts but got {hInfoArr.Length} part(s) instead (Raw: {headerInfoLine})"); - - if (isPatchDir) - { - byte hInfoVer = TryGetVersion(hInfoArr[0]); - if (hInfoVer != 19) throw new FormatException("[Header::TryParseHeaderInfo] HDiff version is unsupported. This patcher only supports the directory patch file with version: 19 only!"); - - if (hInfoArr[1] != "" && !Enum.TryParse(hInfoArr[1], true, out headerInfo.CompMode)) throw new FormatException($"[Header::TryParseHeaderInfo] This patcher doesn't support {hInfoArr[1]} compression!"); - if (string.IsNullOrEmpty(hInfoArr[2])) headerInfo.ChecksumMode = ChecksumMode.NoChecksum; - else if (!Enum.TryParse(hInfoArr[2], true, out headerInfo.ChecksumMode)) throw new FormatException($"[Header::TryParseHeaderInfo] This patcher doesn't support {hInfoArr[2]} checksum!"); - HDiffPatch.Event.PushLog($"[Header::TryParseHeaderInfo] Version: {hInfoVer} ChecksumMode: {headerInfo.ChecksumMode} Compression: {headerInfo.CompMode}", Verbosity.Debug); - - TryReadHeaderAndReferenceInfo(sr, ref headerInfo, ref referenceInfo); - TryReadExternReferenceInfo(sr, diffPath, ref headerInfo, ref referenceInfo); - } - else - { - TryReadNonSingleFileHeaderInfo(sr, diffPath, ref headerInfo); - } - - return isPatchDir; - } - - private static void TryReadExternReferenceInfo(Stream sr, string diffPath, ref HeaderInfo headerInfo, ref DataReferenceInfo referenceInfo) - { - long curPos = sr.Position; - referenceInfo.HeadDataOffset = curPos; - - curPos += referenceInfo.HeadDataCompressedSize > 0 ? referenceInfo.HeadDataCompressedSize : referenceInfo.HeadDataSize; - referenceInfo.PrivateExternDataOffset = curPos; - - curPos += referenceInfo.PrivateExternDataSize; - referenceInfo.ExternDataOffset = curPos; - - curPos += referenceInfo.ExternDataSize; - referenceInfo.HDiffDataOffset = curPos; - referenceInfo.HDiffDataSize = sr.Length - curPos; - - HDiffPatch.Event.PushLog($"[Header::TryReadExternReferenceInfo] headDataOffset: {referenceInfo.HeadDataOffset} | privateExternDataOffset: {referenceInfo.PrivateExternDataOffset} | externDataOffset: {referenceInfo.ExternDataOffset} | hdiffDataOffset: {referenceInfo.HDiffDataOffset} | hdiffDataSize: {referenceInfo.HDiffDataSize}", Verbosity.Debug); - - TryIdentifyDiffType(sr, diffPath, ref headerInfo, ref referenceInfo); - } - - private static void TryIdentifyDiffType(Stream sr, string diffPath, ref HeaderInfo headerInfo, ref DataReferenceInfo referenceInfo) - { - sr.Position = referenceInfo.HDiffDataOffset; - string singleCompressedHeaderLine = sr.ReadStringToNull(); - string[] singleCompressedHeaderArr = singleCompressedHeaderLine.Split('&'); - - // ReSharper disable once AssignmentInConditionalExpression - if (headerInfo.IsSingleCompressedDiff = -#if !(NETSTANDARD2_0 || NET461_OR_GREATER) - singleCompressedHeaderArr[0].AsSpan() is "HDIFFSF20") -#else - singleCompressedHeaderArr[0] == "HDIFFSF20") -#endif - { - TryReadSingleFileHeaderInfo(sr, diffPath, ref headerInfo, referenceInfo); - return; - } - - HDiffPatch.Event.PushLog($"[Header::TryIdentifyDiffType] HDIFF Dir Signature: {singleCompressedHeaderLine}", Verbosity.Debug); - - if (singleCompressedHeaderArr[1] != "" && !Enum.TryParse(singleCompressedHeaderArr[1], true, out headerInfo.CompMode)) throw new FormatException($"[Header::TryIdentifyDiffType] The compression chunk has unsupported compression: {singleCompressedHeaderArr[1]}"); - headerInfo.HeaderMagic = singleCompressedHeaderArr[0]; - - TryReadNonSingleFileHeaderInfo(sr, diffPath, ref headerInfo); - } - - private static void TryReadSingleFileHeaderInfo(Stream sr, string diffPath, ref HeaderInfo headerInfo, DataReferenceInfo referenceInfo) - { - headerInfo.PatchPath = diffPath; - headerInfo.SingleChunkInfo = new DiffSingleChunkInfo(); - - headerInfo.NewDataSize = sr.ReadLong7Bit(); - headerInfo.OldDataSize = sr.ReadLong7Bit(); - - HDiffPatch.Event.PushLog($"[Header::TryReadSingleFileHeaderInfo] oldDataSize: {headerInfo.OldDataSize} | newDataSize: {headerInfo.NewDataSize}", Verbosity.Debug); - - headerInfo.ChunkInfo.CoverCount = sr.ReadLong7Bit(); - headerInfo.StepMemSize = sr.ReadLong7Bit(); - headerInfo.SingleChunkInfo.UncompressedSize = sr.ReadLong7Bit(); - headerInfo.SingleChunkInfo.CompressedSize = sr.ReadLong7Bit(); - headerInfo.SingleChunkInfo.DiffDataPos = sr.Position - referenceInfo.HDiffDataOffset; - - headerInfo.CompressedCount = headerInfo.SingleChunkInfo.CompressedSize > 0 ? 1 : 0; - - HDiffPatch.Event.PushLog($"[Header::TryReadSingleFileHeaderInfo] compressedCount: {headerInfo.CompressedCount}", Verbosity.Debug); - } - - private static void TryReadNonSingleFileHeaderInfo(Stream sr, string diffPath, ref HeaderInfo headerInfo) - { - headerInfo.PatchPath = diffPath; - - long typeEndPos = sr.Position; - headerInfo.NewDataSize = sr.ReadLong7Bit(); - headerInfo.OldDataSize = sr.ReadLong7Bit(); - - HDiffPatch.Event.PushLog($"[Header::TryReadNonSingleFileHeaderInfo] oldDataSize: {headerInfo.OldDataSize} | newDataSize: {headerInfo.NewDataSize}", Verbosity.Debug); - - GetDiffChunkInfo(sr, out headerInfo.ChunkInfo, typeEndPos); - - headerInfo.CompressedCount = (headerInfo.ChunkInfo.CompressCoverBufSize > 1 ? 1 : 0) - + (headerInfo.ChunkInfo.CompressRleCtrlBufSize > 1 ? 1 : 0) - + (headerInfo.ChunkInfo.CompressRleCodeBufSize > 1 ? 1 : 0) - + (headerInfo.ChunkInfo.CompressNewDataDiffSize > 1 ? 1 : 0); - - HDiffPatch.Event.PushLog($"[Header::TryReadNonSingleFileHeaderInfo] compressedCount: {headerInfo.CompressedCount}", Verbosity.Debug); - } - - private static void GetDiffChunkInfo(Stream sr, out DiffChunkInfo chunkInfo, long typeEndPos) - { - chunkInfo = new DiffChunkInfo(); - - HDiffPatch.Event.PushLog($"[Header::GetDiffChunkInfo] typesEndPos: {typeEndPos}", Verbosity.Debug); - - chunkInfo.CoverCount = sr.ReadLong7Bit(); - chunkInfo.CompressSizeBeginPos = sr.Position; - - HDiffPatch.Event.PushLog($"[Header::GetDiffChunkInfo] coverCount: {chunkInfo.CoverCount} | compressSizeBeginPos: {chunkInfo.CompressSizeBeginPos}", Verbosity.Debug); - - chunkInfo.CoverBufSize = sr.ReadLong7Bit(); - chunkInfo.CompressCoverBufSize = sr.ReadLong7Bit(); - chunkInfo.RleCtrlBufSize = sr.ReadLong7Bit(); - chunkInfo.CompressRleCtrlBufSize = sr.ReadLong7Bit(); - chunkInfo.RleCodeBufSize = sr.ReadLong7Bit(); - chunkInfo.CompressRleCodeBufSize = sr.ReadLong7Bit(); - chunkInfo.NewDataDiffSize = sr.ReadLong7Bit(); - chunkInfo.CompressNewDataDiffSize = sr.ReadLong7Bit(); - - HDiffPatch.Event.PushLog($"[Header::GetDiffChunkInfo] cover_buf_size: {chunkInfo.CoverBufSize} | compress_cover_buf_size: {chunkInfo.CompressCoverBufSize}", Verbosity.Debug); - HDiffPatch.Event.PushLog($"[Header::GetDiffChunkInfo] rle_ctrlBuf_size: {chunkInfo.RleCtrlBufSize} | compress_rle_ctrlBuf_size: {chunkInfo.CompressRleCtrlBufSize}", Verbosity.Debug); - HDiffPatch.Event.PushLog($"[Header::GetDiffChunkInfo] rle_codeBuf_size: {chunkInfo.RleCodeBufSize} | compress_rle_codeBuf_size: {chunkInfo.CompressRleCodeBufSize}", Verbosity.Debug); - HDiffPatch.Event.PushLog($"[Header::GetDiffChunkInfo] newDataDiff_size: {chunkInfo.NewDataDiffSize} | compress_newDataDiff_size: {chunkInfo.CompressNewDataDiffSize}", Verbosity.Debug); - - chunkInfo.HeadEndPos = sr.Position; - chunkInfo.CoverEndPos = chunkInfo.HeadEndPos + - (chunkInfo.CompressCoverBufSize > 0 ? - chunkInfo.CompressCoverBufSize : - chunkInfo.CoverBufSize); - - HDiffPatch.Event.PushLog($"[Header::GetDiffChunkInfo] headEndPos: {chunkInfo.HeadEndPos} | coverEndPos: {chunkInfo.CoverEndPos}", Verbosity.Debug); - } - - private static void TryReadHeaderAndReferenceInfo(Stream sr, ref HeaderInfo headerInfo, ref DataReferenceInfo referenceInfo) - { - headerInfo.IsInputDir = sr.ReadBoolean(); - headerInfo.IsOutputDir = sr.ReadBoolean(); - - HDiffPatch.Event.PushLog($"[Header::TryReadHeaderAndReferenceInfo] Is In/Out a Dir -> Input: {headerInfo.IsInputDir} / Output: {headerInfo.IsOutputDir}", Verbosity.Debug); - - referenceInfo.InputDirCount = sr.ReadLong7Bit(); - referenceInfo.InputSumSize = sr.ReadLong7Bit(); - - referenceInfo.OutputDirCount = sr.ReadLong7Bit(); - referenceInfo.OutputSumSize = sr.ReadLong7Bit(); - - HDiffPatch.Event.PushLog($"[Header::TryReadHeaderAndReferenceInfo] InDir Count/SumSize: {referenceInfo.InputDirCount}/{referenceInfo.InputSumSize} | OutDir Count/SumSize: {referenceInfo.OutputSumSize}/{referenceInfo.InputSumSize}", Verbosity.Debug); - - referenceInfo.InputRefFileCount = sr.ReadLong7Bit(); - referenceInfo.InputRefFileSize = sr.ReadLong7Bit(); - - referenceInfo.OutputRefFileCount = sr.ReadLong7Bit(); - referenceInfo.OutputRefFileSize = sr.ReadLong7Bit(); - - HDiffPatch.Event.PushLog($"[Header::TryReadHeaderAndReferenceInfo] InRef Count/Size: {referenceInfo.InputRefFileCount}/{referenceInfo.InputRefFileSize} | OutRef Count/Size: {referenceInfo.OutputRefFileCount}/{referenceInfo.OutputRefFileSize}", Verbosity.Debug); - - referenceInfo.SameFilePairCount = sr.ReadLong7Bit(); - referenceInfo.SameFileSize = sr.ReadLong7Bit(); - - HDiffPatch.Event.PushLog($"[Header::TryReadHeaderAndReferenceInfo] IdenticalPair Count/Size: {referenceInfo.SameFilePairCount}/{referenceInfo.SameFileSize}", Verbosity.Debug); - - referenceInfo.NewExecuteCount = sr.ReadInt7Bit(); - referenceInfo.PrivateReservedDataSize = sr.ReadLong7Bit(); - referenceInfo.PrivateExternDataSize = sr.ReadLong7Bit(); - referenceInfo.ExternDataSize = sr.ReadLong7Bit(); - - HDiffPatch.Event.PushLog($"[Header::TryReadHeaderAndReferenceInfo] newExecuteCount: {referenceInfo.NewExecuteCount} | privateReservedDataSize: {referenceInfo.PrivateReservedDataSize} | privateExternDataSize: {referenceInfo.PrivateExternDataSize} | privateExternDataSize: {referenceInfo.ExternDataSize}", Verbosity.Debug); - - referenceInfo.CompressSizeBeginPos = sr.Position; - - referenceInfo.HeadDataSize = sr.ReadLong7Bit(); - referenceInfo.HeadDataCompressedSize = sr.ReadLong7Bit(); - referenceInfo.ChecksumByteSize = (byte)sr.ReadLong7Bit(); - headerInfo.DirDataIsCompressed = referenceInfo.HeadDataCompressedSize > 0; - referenceInfo.ChecksumOffset = sr.Position; - - HDiffPatch.Event.PushLog($"[Header::TryReadHeaderAndReferenceInfo] compressSizeBeginPos: {referenceInfo.CompressSizeBeginPos} | headDataSize: {referenceInfo.HeadDataSize} | headDataCompressedSize: {referenceInfo.HeadDataCompressedSize} | checksumByteSize: {referenceInfo.ChecksumByteSize}", Verbosity.Debug); - HDiffPatch.Event.PushLog($"[Header::TryReadHeaderAndReferenceInfo] checksumOffset: {referenceInfo.ChecksumOffset} | dirDataIsCompressed: {headerInfo.DirDataIsCompressed}", Verbosity.Debug); - - if (referenceInfo.ChecksumByteSize > 0) - { - HDiffPatch.Event.PushLog($"[Header::TryReadHeaderAndReferenceInfo] Seeking += {referenceInfo.ChecksumByteSize * 4} bytes from checksum bytes!", Verbosity.Debug); - TrySeekHeader(sr, referenceInfo.ChecksumByteSize * 4); - } - } - - private static void TrySeekHeader(Stream sr, int skipLongSize) - { - int len = Math.Min(4 << 10, skipLongSize); - HDiffPatch.Event.PushLog($"[Header::TrySeekHeader] Seeking from: {sr.Position} += {skipLongSize} to {sr.Position + skipLongSize}", Verbosity.Debug); - sr.Seek(len, SeekOrigin.Current); - } - -#if !(NETSTANDARD2_0 || NET461_OR_GREATER) - private static byte TryGetVersion(ReadOnlySpan str) - { - int lastIndexOf = str.IndexOf(HdiffHead); - if (lastIndexOf < 0) throw new IndexOutOfRangeException($"[Header::TryGetVersion] Version string is invalid! Cannot find the matching start of \"HDIFF\". Getting: {str.ToString()} instead"); - - ReadOnlySpan numSpan = str.Slice(lastIndexOf + HdiffHead.Length); - if (byte.TryParse(numSpan, out byte ret)) return ret; - - throw new InvalidDataException($"[Header::TryGetVersion] Version string is invalid! Value: {numSpan.ToString()} (Raw: {str.ToString()})"); - } -#else - private static byte TryGetVersion(string str) - { - int lastIndexOf = str.IndexOf(HdiffHead, StringComparison.OrdinalIgnoreCase); - if (lastIndexOf < 0) throw new IndexOutOfRangeException($"[Header::TryGetVersion] Version string is invalid! Cannot find the matching start of \"HDIFF\". Getting: {str} instead"); - - string numStr = str.Substring(lastIndexOf + HdiffHead.Length); - if (byte.TryParse(numStr, out byte ret)) return ret; - - throw new InvalidDataException($"[Header::TryGetVersion] Version string is invalid! Value: {numStr} (Raw: {str})"); - } -#endif - } -} diff --git a/SharpHDiffPatch.Core/Patch/IPatch.cs b/SharpHDiffPatch.Core/Patch/IPatch.cs deleted file mode 100644 index 26f1054..0000000 --- a/SharpHDiffPatch.Core/Patch/IPatch.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace SharpHDiffPatch.Core.Patch; - -public interface IPatch -{ - void Patch(string input, string output, Action writeBytesDelegate, bool useBufferedPatch, bool useFullBuffer, bool useFastBuffer); -} diff --git a/SharpHDiffPatch.Core/Patch/PatchCore.cs b/SharpHDiffPatch.Core/Patch/PatchCore.cs deleted file mode 100644 index 92e56d2..0000000 --- a/SharpHDiffPatch.Core/Patch/PatchCore.cs +++ /dev/null @@ -1,638 +0,0 @@ -using SharpHDiffPatch.Core.Binary; -using SharpHDiffPatch.Core.Binary.Compression; -using System; -using System.Buffers; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -#if !(NETSTANDARD2_0 || NET461_OR_GREATER) -using System.Runtime.Intrinsics; -using System.Runtime.Intrinsics.Arm; -using System.Runtime.Intrinsics.X86; -#endif -using System.Threading; -using System.Threading.Tasks; -// ReSharper disable ForCanBeConvertedToForeach -// ReSharper disable ConvertIfStatementToSwitchStatement - -#nullable enable -namespace SharpHDiffPatch.Core.Patch -{ - internal struct RleRefClipStruct - { - public long MemCopyLength; - public long MemSetLength; - public byte MemSetValue; - } - - internal readonly struct CoverHeader - { - internal readonly long OldPos; - internal readonly long NewPos; - internal readonly long CoverLength; - internal readonly long NextCoverIndex; - - internal CoverHeader(long oldPos, long newPos, long coverLength, long nextCoverIndex) - { - OldPos = oldPos; - NewPos = newPos; - CoverLength = coverLength; - NextCoverIndex = nextCoverIndex; - } - } - - internal interface IPatchCore - { - void SetDirectoryReferencePair(DirectoryReferencePair pair); - - void SetSizeToBePatched(long sizeToBePatched, long sizeToPatch = 0); - - void UncoverBufferClipsStream(Stream[] clips, Stream inputStream, Stream outputStream, HeaderInfo headerInfo); - - Stream GetBufferStreamFromOffset(HDiffCompressionMode compMode, Stream sourceStream, - long start, long length, long compLength, out long outLength, bool isBuffered, bool isFastBufferUsed); - } - - internal sealed class PatchCore : IPatchCore - { - internal unsafe delegate void RleProc(ref RleRefClipStruct rleLoader, MemoryStream outCache, ref long copyLength, int decodeStep, byte* rlePtr, byte[] rleBuffer, int rleBufferIdx, byte* oldPtr); - internal static RleProc RleProcDelegate; - - internal const int KSignTagBit = 1; - internal const int KByteRleType = 2; - internal const int MaxMemBufferLen = 7 << 20; - internal const int MaxMemBufferLimit = 10 << 20; - internal const int MaxArrayCopyLen = 1 << 18; - internal const int MaxArrayPoolLen = 4 << 20; - internal const int MaxArrayPoolSecondOffset = MaxArrayPoolLen / 2; - - internal CancellationToken Token; - internal long SizeToBePatched; - internal long SizePatched; - internal Stopwatch Stopwatch; - internal string PathInput; - internal string PathOutput; - internal DirectoryReferencePair? DirReferencePair; - - private readonly Action? _writeBytesDelegate; - - static unsafe PatchCore() - { - RleProcDelegate = -#if !(NETSTANDARD2_0 || NET461_OR_GREATER) - Sse2.IsSupported ? - TBytesSetRleVectorSse2Simd : -#if NET7_0_OR_GREATER - AdvSimd.IsSupported && Vector128.IsHardwareAccelerated ? - TBytesSetRleVectorAdvSimd128 : - AdvSimd.IsSupported && Vector64.IsHardwareAccelerated ? - TBytesSetRleVectorAdvSimd64 : -#else - AdvSimd.IsSupported ? - TBytesSetRleVectorAdvSimd64 : -#endif -#endif - TBytesSetRleVectorSoftware; - } - - internal PatchCore(long sizeToBePatched, Stopwatch stopwatch, string inputPath, string outputPath, Action? writeBytesDelegate, CancellationToken token) - { - Token = token; - SizeToBePatched = sizeToBePatched; - Stopwatch = stopwatch; - SizePatched = 0; - PathInput = inputPath; - PathOutput = outputPath; - _writeBytesDelegate = writeBytesDelegate; - } - - public void SetDirectoryReferencePair(DirectoryReferencePair pair) => DirReferencePair = pair; - - public void SetSizeToBePatched(long sizeToBePatched, long sizeToPatch = 0) - { - SizeToBePatched = sizeToBePatched; - SizePatched = sizeToPatch; - } - - public Stream GetBufferStreamFromOffset(HDiffCompressionMode compMode, Stream sourceStream, - long start, long length, long compLength, out long outLength, bool isBuffered, bool isFastBufferUsed) - { - sourceStream.Position = start; - - CompressionStreamHelper.GetDecompressStreamPlugin(compMode, sourceStream, out Stream returnStream, length, compLength, out outLength, isBuffered); - - if (!isBuffered || isFastBufferUsed) return returnStream; - HDiffPatch.Event.PushLog($"[PatchCore::GetBufferStreamFromOffset] Caching stream from offset: {start} with length: {(compLength > 0 ? compLength : length)}"); - using (returnStream) - { - MemoryStream stream = CreateAndCopyToMemoryStream(returnStream); - stream.Position = 0; - return stream; - } - - } - - private MemoryStream CreateAndCopyToMemoryStream(Stream source) - { - MemoryStream returnStream = new(); - byte[] buffer = ArrayPool.Shared.Rent(MaxArrayPoolLen); - - try - { - int read; -#if !(NETSTANDARD2_0 || NET461_OR_GREATER) - while ((read = source.Read(buffer)) > 0) -#else - while ((read = source.Read(buffer, 0, buffer.Length)) > 0) -#endif - { - Token.ThrowIfCancellationRequested(); - returnStream.Write(buffer, 0, read); - } - - return returnStream; - } - finally - { - ArrayPool.Shared.Return(buffer); - } - } - - public void UncoverBufferClipsStream(Stream[] clips, Stream inputStream, Stream outputStream, HeaderInfo headerInfo) => WriteCoverStreamToOutput(clips, inputStream, outputStream, headerInfo.ChunkInfo.CoverCount, headerInfo.ChunkInfo.CoverBufSize, headerInfo.NewDataSize); - - internal static IEnumerable EnumerateCoverHeaders(Stream coverReader, long coverSize, long coverCount) - { - long lastOldPosBack = 0, - lastNewPosBack = 0; - - if (coverSize < MaxMemBufferLen) - { - HDiffPatch.Event.PushLog($"[PatchCore::EnumerateCoverHeaders] Enumerate cover counts from buffer with size: {coverSize}", Verbosity.Verbose); - byte[] buffer = new byte[coverSize]; - coverReader.ReadExactly(buffer, 0, buffer.Length); - - int offset = 0; - while (coverCount-- > 0) - { - long oldPosBack = lastOldPosBack; - long newPosBack = lastNewPosBack; - - byte pSign = buffer[offset++]; - - byte incOldPosSign = (byte)(pSign >> (8 - KSignTagBit)); - long incOldPos = buffer.ReadLong7Bit(ref offset, KSignTagBit, pSign); - long oldPos = incOldPosSign == 0 ? oldPosBack + incOldPos : oldPosBack - incOldPos; - - long copyLength = buffer.ReadLong7Bit(ref offset); - long coverLength = buffer.ReadLong7Bit(ref offset); - oldPosBack = oldPos; - newPosBack += copyLength; - - oldPosBack += coverLength; - - yield return new CoverHeader(oldPos, newPosBack, coverLength, coverCount); - newPosBack += coverLength; - - lastOldPosBack = oldPosBack; - lastNewPosBack = newPosBack; - } - } - else - { - HDiffPatch.Event.PushLog($"[PatchCore::EnumerateCoverHeaders] Enumerate cover counts directly from stream with size: {coverSize}", Verbosity.Verbose); - while (coverCount-- > 0) - { - long oldPosBack = lastOldPosBack; - long newPosBack = lastNewPosBack; - - byte pSign = (byte)coverReader.ReadByte(); - - byte incOldPosSign = (byte)(pSign >> (8 - KSignTagBit)); - long incOldPos = coverReader.ReadLong7Bit(KSignTagBit, pSign); - long oldPos = incOldPosSign == 0 ? oldPosBack + incOldPos : oldPosBack - incOldPos; - - long copyLength = coverReader.ReadLong7Bit(); - long coverLength = coverReader.ReadLong7Bit(); - newPosBack += copyLength; - oldPosBack = oldPos; - - oldPosBack += coverLength; - - yield return new CoverHeader(oldPos, newPosBack, coverLength, coverCount); - newPosBack += coverLength; - - lastOldPosBack = oldPosBack; - lastNewPosBack = newPosBack; - } - } - } - - internal void RunCopySimilarFilesRoutine() - { - if (DirReferencePair == null) return; - HDiffPatch.Event.PushLog("Start copying similar data"); - CopyOldSimilarToNewFiles(DirReferencePair); - - TimeSpan timeTaken = Stopwatch.Elapsed; - HDiffPatch.Event.PushLog($"Copying similar data has been finished in {timeTaken.TotalSeconds} seconds ({timeTaken.TotalMilliseconds} ms)"); - HDiffPatch.Event.PushLog("Starting patching process..."); - Stopwatch.Restart(); - } - - private void WriteCoverStreamToOutput(Stream[] clips, Stream inputStream, Stream outputStream, long coverCount, long coverSize, long newDataSize) - { - byte[] sharedBuffer = ArrayPool.Shared.Rent(MaxArrayPoolLen); - MemoryStream cacheOutputStream = new(); - - try - { - RunCopySimilarFilesRoutine(); - - long newPosBack = 0; - RleRefClipStruct rleStruct = new(); - CoverHeader[] headers = [.. EnumerateCoverHeaders(clips[0], coverSize, coverCount)]; - - for (int i = 0; i < headers.Length; i++) - { - CoverHeader cover = headers[i]; - - Token.ThrowIfCancellationRequested(); - - if (newPosBack < cover.NewPos) - { - long copyLength = cover.NewPos - newPosBack; - inputStream.Position = cover.OldPos; - - TBytesCopyStreamFromOldClip(cacheOutputStream, clips[3], copyLength, sharedBuffer); - TBytesDetermineRleType(ref rleStruct, cacheOutputStream, copyLength, sharedBuffer, clips[1], clips[2]); - } - - TBytesCopyOldClipPatch(cacheOutputStream, inputStream, ref rleStruct, cover.OldPos, cover.CoverLength, sharedBuffer, clips[1], clips[2]); - newPosBack = cover.NewPos + cover.CoverLength; - - if (cacheOutputStream.Length > MaxMemBufferLimit || cover.NextCoverIndex == 0) - WriteInMemoryOutputToStream(cacheOutputStream, outputStream); - } - - if (newPosBack < newDataSize) - { - long copyLength = newDataSize - newPosBack; - TBytesCopyStreamFromOldClip(cacheOutputStream, clips[3], copyLength, sharedBuffer); - TBytesDetermineRleType(ref rleStruct, cacheOutputStream, copyLength, sharedBuffer, clips[1], clips[2]); - WriteInMemoryOutputToStream(cacheOutputStream, outputStream); - } - - SpawnCorePatchFinishedMsg(); - } - finally - { - ArrayPool.Shared.Return(sharedBuffer); - Stopwatch.Stop(); - cacheOutputStream.Dispose(); - for (int i = 0; i < clips.Length; i++) clips[i].Dispose(); - inputStream.Dispose(); - outputStream.Dispose(); - } - } - - internal void WriteInMemoryOutputToStream(MemoryStream cacheOutputStream, Stream outputStream) - { - long oldPos = outputStream.Position; - - cacheOutputStream.Position = 0; - cacheOutputStream.CopyTo(outputStream); - cacheOutputStream.SetLength(0); - - long newPos = outputStream.Position; - long read = newPos - oldPos; - - _writeBytesDelegate?.Invoke(read); - HDiffPatch.UpdateEvent(read, ref SizePatched, ref SizeToBePatched, Stopwatch); - } - - internal void SpawnCorePatchFinishedMsg() - { - TimeSpan timeTaken = Stopwatch.Elapsed; - HDiffPatch.Event.PushLog($"Patching has been finished in {timeTaken.TotalSeconds} seconds ({timeTaken.TotalMilliseconds} ms)"); - } - - private static void TBytesCopyOldClipPatch(MemoryStream outCache, Stream inputStream, ref RleRefClipStruct rleLoader, long oldPos, long addLength, byte[] sharedBuffer, - Stream rleCtrlStream, Stream rleCodeStream) - { - long lastPos = outCache.Position; - inputStream.Position = oldPos; - - TBytesCopyStreamInner(inputStream, outCache, sharedBuffer, (int)addLength); - - outCache.Position = lastPos; - TBytesDetermineRleType(ref rleLoader, outCache, addLength, sharedBuffer, rleCtrlStream, rleCodeStream); - } - - internal static void TBytesCopyStreamFromOldClip(MemoryStream outCache, Stream copyReader, long copyLength, byte[] sharedBuffer) - { - long lastPos = outCache.Position; - TBytesCopyStreamInner(copyReader, outCache, sharedBuffer, (int)copyLength); - outCache.Position = lastPos; - } - - internal static void TBytesCopyStreamInner(Stream input, MemoryStream output, byte[] sharedBuffer, int readLen) - { - AddBytesCopy: - int toRead = Math.Min(sharedBuffer.Length, readLen); - input.ReadExactly(sharedBuffer, 0, toRead); - output.Write(sharedBuffer, 0, toRead); - readLen -= toRead; - if (toRead != 0) goto AddBytesCopy; - } - - private static void TBytesDetermineRleType(ref RleRefClipStruct rleLoader, MemoryStream outCache, long copyLength, byte[] sharedBuffer, - Stream rleCtrlStream, Stream rleCodeStream) - { - TBytesSetRle(ref rleLoader, outCache, ref copyLength, sharedBuffer, rleCodeStream); - - while (copyLength > 0) - { - byte pSign = (byte)rleCtrlStream.ReadByte(); - byte type = (byte)(pSign >> (8 - KByteRleType)); - long length = rleCtrlStream.ReadLong7Bit(KByteRleType, pSign); - ++length; - - if (type == 3) - { - rleLoader.MemCopyLength = length; - TBytesSetRle(ref rleLoader, outCache, ref copyLength, sharedBuffer, rleCodeStream); - continue; - } - - rleLoader.MemSetLength = length; - if (type == 2) - { - rleLoader.MemSetValue = (byte)rleCodeStream.ReadByte(); - TBytesSetRle(ref rleLoader, outCache, ref copyLength, sharedBuffer, rleCodeStream); - continue; - } - - /* If the type is 1, then 0 - 1. This should result -1 in int but since - * we cast it to byte, then it underflow and set it to 255. - * This method is the same as: - * if (type == 0) - * rleLoader.memSetValue = 0x00; // or 0 in byte - * else - * rleLoader.memSetValue = 0xFF; // or 255 in byte - */ - rleLoader.MemSetValue = (byte)(0x00 - type); - TBytesSetRle(ref rleLoader, outCache, ref copyLength, sharedBuffer, rleCodeStream); - } - } - - private static unsafe void TBytesSetRle(ref RleRefClipStruct rleLoader, MemoryStream outCache, ref long copyLength, byte[] sharedBuffer, Stream rleCodeStream) - { - TBytesSetRleSingle(ref rleLoader, outCache, ref copyLength, sharedBuffer); - - if (rleLoader.MemCopyLength == 0) return; - int decodeStep = (int)(rleLoader.MemCopyLength > copyLength ? copyLength : rleLoader.MemCopyLength); - - long lastPosCopy = outCache.Position; - rleCodeStream.ReadExactly(sharedBuffer, 0, decodeStep); - outCache.ReadExactly(sharedBuffer, MaxArrayPoolSecondOffset, decodeStep); - outCache.Position = lastPosCopy; - - fixed (byte* rlePtr = &sharedBuffer[0], oldPtr = &sharedBuffer[MaxArrayPoolSecondOffset]) - { - RleProcDelegate(ref rleLoader, outCache, ref copyLength, decodeStep, rlePtr, sharedBuffer, 0, oldPtr); - } - } - - internal static void TBytesSetRleSingle(ref RleRefClipStruct rleLoader, MemoryStream outCache, ref long copyLength, byte[] sharedBuffer) - { - if (rleLoader.MemSetLength == 0) return; - long memSetStep = rleLoader.MemSetLength <= copyLength ? rleLoader.MemSetLength : copyLength; - if (rleLoader.MemSetValue != 0) - { - int length = (int)memSetStep; - long lastPos = outCache.Position; - _ = outCache.Read(sharedBuffer, 0, length); - outCache.Position = lastPos; - - SetAddRLESingle: - sharedBuffer[--length] += rleLoader.MemSetValue; - if (length > 0) goto SetAddRLESingle; - - outCache.Write(sharedBuffer, 0, (int)memSetStep); - } - else - { - outCache.Position += memSetStep; - } - - copyLength -= memSetStep; - rleLoader.MemSetLength -= memSetStep; - } - - internal static unsafe void TBytesSetRleVectorSoftware(ref RleRefClipStruct rleLoader, MemoryStream outCache, ref long copyLength, int decodeStep, byte* rlePtr, byte[] rleBuffer, int rleBufferIdx, byte* oldPtr) - { - int index = 0; - - AddRleSoftware: - *(rlePtr + index) += *(oldPtr + index); - if (++index < decodeStep) goto AddRleSoftware; - - WriteRleResultToStream(ref rleLoader, outCache, rleBuffer, rleBufferIdx, ref copyLength, decodeStep); - } - -#if !(NETSTANDARD2_0 || NET461_OR_GREATER) - internal static unsafe void TBytesSetRleVectorAdvSimd128(ref RleRefClipStruct rleLoader, MemoryStream outCache, ref long copyLength, int decodeStep, byte* rlePtr, byte[] rleBuffer, int rleBufferIdx, byte* oldPtr) - { - int len = decodeStep; - - if (len >= Vector128.Count) - { - AddVectorArm64_128: - len -= Vector128.Count; - Vector128 resultVector = AdvSimd.Add(*(Vector128*)(rlePtr + len), *(Vector128*)(oldPtr + len)); - AdvSimd.Store(rlePtr + len, resultVector); - if (len >= Vector128.Count) goto AddVectorArm64_128; - } - - WriteRemainedRleSimdResultToStream(ref rleLoader, len, outCache, ref copyLength, decodeStep, rlePtr, rleBuffer, rleBufferIdx, oldPtr); - } - - - internal static unsafe void TBytesSetRleVectorAdvSimd64(ref RleRefClipStruct rleLoader, MemoryStream outCache, ref long copyLength, int decodeStep, byte* rlePtr, byte[] rleBuffer, int rleBufferIdx, byte* oldPtr) - { - int len = decodeStep; - - if (len >= Vector64.Count) - { - AddVectorArm64_64: - len -= Vector64.Count; - Vector64 resultVector = AdvSimd.Add(*(Vector64*)(rlePtr + len), *(Vector64*)(oldPtr + len)); - AdvSimd.Store(rlePtr + len, resultVector); - if (len >= Vector64.Count) goto AddVectorArm64_64; - } - - WriteRemainedRleSimdResultToStream(ref rleLoader, len, outCache, ref copyLength, decodeStep, rlePtr, rleBuffer, rleBufferIdx, oldPtr); - } - - internal static unsafe void TBytesSetRleVectorSse2Simd(ref RleRefClipStruct rleLoader, MemoryStream outCache, ref long copyLength, int decodeStep, byte* rlePtr, byte[] rleBuffer, int rleBufferIdx, byte* oldPtr) - { - int len = decodeStep; - - if (len >= Vector128.Count) - { - AddVectorSse2: - len -= Vector128.Count; - Vector128 resultVector = Sse2.Add(*(Vector128*)(rlePtr + len), *(Vector128*)(oldPtr + len)); - Sse2.Store(rlePtr + len, resultVector); - if (len >= Vector128.Count) goto AddVectorSse2; - } - - WriteRemainedRleSimdResultToStream(ref rleLoader, len, outCache, ref copyLength, decodeStep, rlePtr, rleBuffer, rleBufferIdx, oldPtr); - } - - private static unsafe void WriteRemainedRleSimdResultToStream(ref RleRefClipStruct rleLoader, int len, MemoryStream outCache, ref long copyLength, int decodeStep, byte* rlePtr, byte[] rleBuffer, int rleBufferIdx, byte* oldPtr) - { - if (len >= 4) - { - AddRemainsFourStep: - len -= 4; - *(rlePtr + len) += *(oldPtr + len); - *(rlePtr + 1 + len) += *(oldPtr + 1 + len); - *(rlePtr + 2 + len) += *(oldPtr + 2 + len); - *(rlePtr + 3 + len) += *(oldPtr + 3 + len); - if (len >= 4) goto AddRemainsFourStep; - } - - AddRemainsVectorRLE: - if (len == 0) goto WriteAllVectorRLE; - *(rlePtr + --len) += *(oldPtr + len); - goto AddRemainsVectorRLE; - - WriteAllVectorRLE: - WriteRleResultToStream(ref rleLoader, outCache, rleBuffer, rleBufferIdx, ref copyLength, decodeStep); - } -#endif - - private static void WriteRleResultToStream(ref RleRefClipStruct rleLoader, MemoryStream outCache, byte[] rleBuffer, int rleBufferIdx, ref long copyLength, int decodeStep) - { - outCache.Write(rleBuffer, rleBufferIdx, decodeStep); - - rleLoader.MemCopyLength -= decodeStep; - copyLength -= decodeStep; - } - - internal static bool IsPathADir( -#if !(NETSTANDARD2_0 || NET461_OR_GREATER) - ReadOnlySpan -#else - string -#endif - // ReSharper disable once UseIndexFromEndExpression - input) => input.Length == 0 || input[input.Length - 1] == '/'; - - internal static ref string NewPathByIndex(string[] source, long index) => ref source[index]; - - private void CopyOldSimilarToNewFiles(DirectoryReferencePair dirData) - { - int curNewRefIndex = 0; - int curPathIndex = 0; - int curSamePairIndex = 0; - int newRefCount = dirData.NewRefList.Length; - int samePairCount = dirData.DataSamePairList.Length; - int pathCount = dirData.NewUtf8PathList.Length; - - try - { - Parallel.ForEach(dirData.DataSamePairList, new ParallelOptions { CancellationToken = Token }, (pair) => - { - bool isPathADir = IsPathADir(dirData.NewUtf8PathList[pair.NewIndex]); - if (isPathADir) return; - - CopyFileByPairIndex(dirData.OldUtf8PathList, dirData.NewUtf8PathList, pair.OldIndex, pair.NewIndex); - }); - } - catch (AggregateException ex) - { - throw ex.Flatten().InnerExceptions.First(); - } - - while (curPathIndex < pathCount) - { - if (curNewRefIndex < newRefCount - && curPathIndex == (dirData.NewRefList.Length > 0 ? (int)dirData.NewRefList[curNewRefIndex] : curNewRefIndex)) - { - bool isPathADir = IsPathADir(dirData.NewUtf8PathList[(int)dirData.NewRefList[curNewRefIndex]]); - - if (isPathADir) ++curPathIndex; - ++curNewRefIndex; - } - else if (curSamePairIndex < samePairCount - && curPathIndex == (int)dirData.DataSamePairList[curSamePairIndex].NewIndex) - { - ++curSamePairIndex; - ++curPathIndex; - } - else - { -#if !(NETSTANDARD2_0 || NET461_OR_GREATER) - ReadOnlySpan -#else - string -#endif - pathByIndex = NewPathByIndex(dirData.NewUtf8PathList, curPathIndex); - string combinedNewPath = Path.Combine(PathOutput, pathByIndex.ToString()); - bool isPathADir = false; - - if (pathByIndex.Length > 0) - { - isPathADir = IsPathADir(pathByIndex); - - if (isPathADir && !Directory.Exists(combinedNewPath)) Directory.CreateDirectory(combinedNewPath); - else if (!isPathADir && !File.Exists(combinedNewPath)) File.Create(combinedNewPath).Dispose(); - } - - HDiffPatch.Event.PushLog($"[PatchCore::CopyOldSimilarToNewFiles] Created a new {(isPathADir ? "directory" : "empty file")}: {combinedNewPath}", Verbosity.Debug); - - ++curPathIndex; - } - } - } - - private void CopyFileByPairIndex(string[] oldList, string[] newList, long oldIndex, long newIndex) - { - ref string oldPath = ref oldList[oldIndex]; - ref string newPath = ref newList[newIndex]; - string oldFullPath = Path.Combine(PathInput, oldPath); - string newFullPath = Path.Combine(PathOutput, newPath); - string? newDirFullPath = Path.GetDirectoryName(newFullPath); - if (!string.IsNullOrEmpty(newDirFullPath)) - Directory.CreateDirectory(newDirFullPath); - - HDiffPatch.Event.PushLog($"[PatchCore::CopyFileByPairIndex] Copying similar file to target path: {oldFullPath} -> {newFullPath}", Verbosity.Debug); - CopyFile(oldFullPath, newFullPath); - } - - private void CopyFile(string inputPath, string outputPath) - { -#if NET6_0_OR_GREATER - byte[] buffer = GC.AllocateUninitializedArray(MaxArrayCopyLen); -#else - byte[] buffer = new byte[MaxArrayCopyLen]; -#endif - using FileStream ifs = new(inputPath, FileMode.Open, FileAccess.Read, FileShare.Read); - using FileStream ofs = new(outputPath, FileMode.Create, FileAccess.Write, FileShare.Write); - int read; -#if !(NETSTANDARD2_0 || NET461_OR_GREATER) - while ((read = ifs.Read(buffer)) > 0) -#else - while ((read = ifs.Read(buffer, 0, buffer.Length)) > 0) -#endif - { - Token.ThrowIfCancellationRequested(); - ofs.Write(buffer, 0, read); - HDiffPatch.UpdateEvent(read, ref SizePatched, ref SizeToBePatched, Stopwatch); - } - } - } -} diff --git a/SharpHDiffPatch.Core/Patch/PatchCoreFastBuffer.cs b/SharpHDiffPatch.Core/Patch/PatchCoreFastBuffer.cs deleted file mode 100644 index 8d28779..0000000 --- a/SharpHDiffPatch.Core/Patch/PatchCoreFastBuffer.cs +++ /dev/null @@ -1,368 +0,0 @@ -using SharpHDiffPatch.Core.Binary; -using SharpHDiffPatch.Core.Binary.Compression; -using System; -using System.Buffers; -using System.Diagnostics; -using System.IO; -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; - -namespace SharpHDiffPatch.Core.Patch -{ - internal sealed class PatchCoreFastBuffer : IPatchCore - { - private const int MaxMemBufferLenBig = 32 << 20; - private const int MaxArrayPoolLen = 4 << 20; - private const int MaxArrayPoolSecondOffset = MaxArrayPoolLen / 2; -#if NET6_0_OR_GREATER - private const int MinUninitializedArrayLen = 2 << 10; -#endif - private readonly PatchCore _core; - - internal PatchCoreFastBuffer(long sizeToBePatched, Stopwatch stopwatch, string inputPath, string outputPath, Action writeBytesDelegate, CancellationToken token) - { - _core = new PatchCore(sizeToBePatched, stopwatch, inputPath, outputPath, writeBytesDelegate, token); - } - - public void SetDirectoryReferencePair(DirectoryReferencePair pair) => _core.SetDirectoryReferencePair(pair); - - public void SetSizeToBePatched(long sizeToBePatched, long sizeToPatch = 0) => _core.SetSizeToBePatched(sizeToBePatched, sizeToPatch); - - public Stream GetBufferStreamFromOffset(HDiffCompressionMode compMode, Stream sourceStream, - long start, long length, long compLength, out long outLength, bool isBuffered, bool isFastBufferUsed) => - _core.GetBufferStreamFromOffset(compMode, sourceStream, start, length, compLength, out outLength, isBuffered, isFastBufferUsed); - - public void UncoverBufferClipsStream(Stream[] clips, Stream inputStream, Stream outputStream, HeaderInfo headerInfo) - { - if (!PatchSizeHelper.CanUseFastBuffer(headerInfo)) - { - HDiffPatch.Event.PushLog("[PatchCoreFastBuffer::UncoverBufferClipsStream] Fast buffer requirements exceeded; delegating to streaming patch core."); - _core.UncoverBufferClipsStream(clips, inputStream, outputStream, headerInfo); - return; - } - - if (_core.DirReferencePair != null) - { - Task[] parallelTasks = - [ - Task.Run(() => WriteCoverStreamToOutputFast(clips, inputStream, outputStream, headerInfo)), - Task.Run(_core.RunCopySimilarFilesRoutine) - ]; - - Task.WaitAll(parallelTasks); - } - else - WriteCoverStreamToOutputFast(clips, inputStream, outputStream, headerInfo); - - _core.SpawnCorePatchFinishedMsg(); - } - - internal static void CreateCoverHeaderAsOutputBuffer(Stream coverStream, byte[] outputBuffer, long coverSize, long coverCount) - { - const int sizeOfLong = sizeof(long) * 4; - const int kSignTagBit = 1; - - if (coverSize == 0) - { - return; - } - - byte[] sevenBitCoverBuffer = ArrayPool.Shared.Rent(PatchSizeHelper.ToCheckedInt32(coverSize, nameof(coverSize))); - ref byte sevenBitBufferRef = ref sevenBitCoverBuffer[0]; - - try - { - int coverSizeInt = PatchSizeHelper.ToCheckedInt32(coverSize, nameof(coverSize)); - coverStream.ReadExactly(sevenBitCoverBuffer, 0, coverSizeInt); - - long lastOldPosBack = 0; - long lastNewPosBack = 0; - - ref long outBufferOldPosRef = ref outputBuffer.AsRef(); - ref long outBufferNewPosRef = ref outputBuffer.AsRef(8); - ref long outBufferCoverLengthRef = ref outputBuffer.AsRef(16); - ref long outBufferNextCoverPosRef = ref outputBuffer.AsRef(24); - - while (coverCount-- > 0) - { - long oldPosBack = lastOldPosBack; - long newPosBack = lastNewPosBack; - - byte pSign = sevenBitBufferRef; - sevenBitBufferRef = ref Unsafe.AddByteOffset(ref sevenBitBufferRef, 1); - - byte incOldPosSign = (byte)(pSign >> (8 - kSignTagBit)); - - sevenBitBufferRef = ref sevenBitBufferRef.ReadLong7Bit(out long incOldPos, kSignTagBit, pSign); - sevenBitBufferRef = ref sevenBitBufferRef.ReadLong7Bit(out long copyLength); - sevenBitBufferRef = ref sevenBitBufferRef.ReadLong7Bit(out long coverLength); - - long oldPos = incOldPosSign == 0 ? oldPosBack + incOldPos : oldPosBack - incOldPos; - - oldPosBack = oldPos + coverLength; - newPosBack += copyLength; - - outBufferOldPosRef = oldPos; - outBufferNewPosRef = newPosBack; - outBufferCoverLengthRef = coverLength; - outBufferNextCoverPosRef = coverCount; - - outBufferOldPosRef = ref Unsafe.AddByteOffset(ref outBufferOldPosRef, sizeOfLong); - outBufferNewPosRef = ref Unsafe.AddByteOffset(ref outBufferNewPosRef, sizeOfLong); - outBufferCoverLengthRef = ref Unsafe.AddByteOffset(ref outBufferCoverLengthRef, sizeOfLong); - outBufferNextCoverPosRef = ref Unsafe.AddByteOffset(ref outBufferNextCoverPosRef, sizeOfLong); - - newPosBack += coverLength; - lastOldPosBack = oldPosBack; - lastNewPosBack = newPosBack; - } - } - finally - { - ArrayPool.Shared.Return(sevenBitCoverBuffer); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ulong GetRoundUpToPowerOf2(ulong value) - { -#if NET6_0_OR_GREATER - return BitOperations.RoundUpToPowerOf2(value); -#else - --value; - value |= value >> 1; - value |= value >> 2; - value |= value >> 4; - value |= value >> 8; - value |= value >> 16; - value |= value >> 32; - return value + 1; -#endif - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static MemoryStream AllocCacheMemoryStream(long newDataSize) - { - ulong roundedSize = GetRoundUpToPowerOf2((ulong)newDataSize); - int bufferSizePow2 = roundedSize > int.MaxValue - ? MaxMemBufferLenBig - : PatchSizeHelper.ToCheckedInt32((long)roundedSize, nameof(newDataSize)); - if (bufferSizePow2 > MaxMemBufferLenBig) - bufferSizePow2 = MaxMemBufferLenBig; - - return new MemoryStream(bufferSizePow2); - } - - private unsafe void WriteCoverStreamToOutputFast(Stream[] clips, Stream inputStream, Stream outputStream, HeaderInfo headerInfo) - { - int rleCtrlIdx = 0, rleCodeIdx = 0; - -#if !NET6_0_OR_GREATER - byte[] sharedBuffer = ArrayPool.Shared.Rent(MaxArrayPoolSecondOffset); - MemoryStream cacheOutputStream = AllocCacheMemoryStream(headerInfo.NewDataSize); - int poolSizeRemained = MaxArrayPoolLen - sharedBuffer.Length; - - bool isCtrlUseArrayPool = headerInfo.ChunkInfo.RleCtrlBufSize <= poolSizeRemained; - int rleCtrlBufSize = PatchSizeHelper.ToCheckedInt32(headerInfo.ChunkInfo.RleCtrlBufSize, nameof(headerInfo.ChunkInfo.RleCtrlBufSize)); - byte[] rleCtrlBuffer = isCtrlUseArrayPool ? ArrayPool.Shared.Rent(rleCtrlBufSize) - : new byte[rleCtrlBufSize]; - poolSizeRemained -= rleCtrlBuffer.Length; - - bool isRleUseArrayPool = headerInfo.ChunkInfo.RleCodeBufSize <= poolSizeRemained; - int rleCodeBufSize = PatchSizeHelper.ToCheckedInt32(headerInfo.ChunkInfo.RleCodeBufSize, nameof(headerInfo.ChunkInfo.RleCodeBufSize)); - byte[] rleCodeBuffer = isRleUseArrayPool ? ArrayPool.Shared.Rent(rleCodeBufSize) - : new byte[rleCodeBufSize]; -#else - byte[] sharedBuffer = GC.AllocateUninitializedArray(MaxArrayPoolSecondOffset); - MemoryStream cacheOutputStream = AllocCacheMemoryStream(headerInfo.NewDataSize); - - int rleCtrlBufSize = PatchSizeHelper.ToCheckedInt32(headerInfo.ChunkInfo.RleCtrlBufSize, nameof(headerInfo.ChunkInfo.RleCtrlBufSize)); - bool isCtrlUseArrayPool = MinUninitializedArrayLen > rleCtrlBufSize; - byte[] rleCtrlBuffer = isCtrlUseArrayPool ? GC.AllocateUninitializedArray(rleCtrlBufSize) - : new byte[rleCtrlBufSize]; - - int rleCodeBufSize = PatchSizeHelper.ToCheckedInt32(headerInfo.ChunkInfo.RleCodeBufSize, nameof(headerInfo.ChunkInfo.RleCodeBufSize)); - bool isRleUseArrayPool = MinUninitializedArrayLen > rleCodeBufSize; - byte[] rleCodeBuffer = isRleUseArrayPool ? GC.AllocateUninitializedArray(rleCodeBufSize) - : new byte[rleCodeBufSize]; -#endif - - RleRefClipStruct rleStruct = new(); - int coverBufferLen = checked(sizeof(CoverHeader) * PatchSizeHelper.ToCheckedInt32(headerInfo.ChunkInfo.CoverCount, nameof(headerInfo.ChunkInfo.CoverCount))); - byte[] coverBuffer = -#if NET6_0_OR_GREATER - GC.AllocateUninitializedArray(coverBufferLen); -#else - new byte[coverBufferLen]; -#endif - - CreateCoverHeaderAsOutputBuffer(clips[0], coverBuffer, headerInfo.ChunkInfo.CoverBufSize, headerInfo.ChunkInfo.CoverCount); - - const int sizeOfCoverHeader = sizeof(long) * 4; - try - { - using (clips[1]) - using (clips[2]) - { - string ctrlStats = - isCtrlUseArrayPool ? -#if !NET6_0_OR_GREATER - "ArrayPool" -#else - "UninitializedArray" -#endif - : "heap buffer"; - string rleStats = - isRleUseArrayPool ? -#if !NET6_0_OR_GREATER - "ArrayPool" -#else - "UninitializedArray" -#endif - : "heap buffer"; - HDiffPatch.Event.PushLog($"[PatchCoreFastBuffer::WriteCoverStreamToOutputFast] Buffering RLE Ctrl clip to {ctrlStats}"); - clips[1].ReadExactly(rleCtrlBuffer, 0, rleCtrlBufSize); - HDiffPatch.Event.PushLog($"[PatchCoreFastBuffer::WriteCoverStreamToOutputFast] Buffering RLE Code clip to {rleStats}"); - clips[2].ReadExactly(rleCodeBuffer, 0, rleCodeBufSize); - } - - long copyLength; - long newPosBack = 0; - if (coverBuffer.Length == 0) - { - goto EndCoverRead; - } - - ref CoverHeader cover = ref coverBuffer.AsRef(); - ref CoverHeader lastCover = ref coverBuffer.AsRef(coverBufferLen - sizeOfCoverHeader); - - StartCoverRead: - if (Unsafe.IsAddressGreaterThan(ref cover, ref lastCover)) - goto EndCoverRead; - - _core.Token.ThrowIfCancellationRequested(); - - if (newPosBack < cover.NewPos) - { - copyLength = cover.NewPos - newPosBack; - inputStream.Position = cover.OldPos; - - PatchCore.TBytesCopyStreamFromOldClip(cacheOutputStream, clips[3], copyLength, sharedBuffer); - TBytesDetermineRleType(ref rleStruct, cacheOutputStream, copyLength, sharedBuffer, rleCtrlBuffer, ref rleCtrlIdx, rleCodeBuffer, ref rleCodeIdx); - } - - TBytesCopyOldClipPatch(cacheOutputStream, inputStream, ref rleStruct, cover.OldPos, cover.CoverLength, sharedBuffer, rleCtrlBuffer, ref rleCtrlIdx, rleCodeBuffer, ref rleCodeIdx); - newPosBack = cover.NewPos + cover.CoverLength; - - if (cacheOutputStream.Length > MaxMemBufferLenBig || cover.NextCoverIndex == 0) - { - _core.WriteInMemoryOutputToStream(cacheOutputStream, outputStream); - } - - cover = ref Unsafe.AddByteOffset(ref cover, sizeOfCoverHeader); - goto StartCoverRead; - - EndCoverRead: - if (newPosBack >= headerInfo.NewDataSize) return; - - copyLength = headerInfo.NewDataSize - newPosBack; - PatchCore.TBytesCopyStreamFromOldClip(cacheOutputStream, clips[3], copyLength, sharedBuffer); - TBytesDetermineRleType(ref rleStruct, cacheOutputStream, copyLength, sharedBuffer, rleCtrlBuffer, ref rleCtrlIdx, rleCodeBuffer, ref rleCodeIdx); - _core.WriteInMemoryOutputToStream(cacheOutputStream, outputStream); - } - finally - { -#if !NET6_0_OR_GREATER - if (sharedBuffer != null) ArrayPool.Shared.Return(sharedBuffer); - if (rleCtrlBuffer != null && isCtrlUseArrayPool) ArrayPool.Shared.Return(rleCtrlBuffer); - if (rleCodeBuffer != null && isRleUseArrayPool) ArrayPool.Shared.Return(rleCodeBuffer); -#endif - _core.Stopwatch.Stop(); - cacheOutputStream.Dispose(); - clips[0].Dispose(); - clips[3].Dispose(); - inputStream.Dispose(); - outputStream.Dispose(); - } - } - - private static void TBytesCopyOldClipPatch(MemoryStream outCache, Stream inputStream, ref RleRefClipStruct rleLoader, long oldPos, long addLength, byte[] sharedBuffer, - ReadOnlySpan rleCtrlBuffer, ref int rleCtrlIdx, byte[] rleCodeBuffer, ref int rleCodeIdx) - { - long lastPos = outCache.Position; - inputStream.Position = oldPos; - - PatchCore.TBytesCopyStreamInner(inputStream, outCache, sharedBuffer, (int)addLength); - - outCache.Position = lastPos; - TBytesDetermineRleType(ref rleLoader, outCache, addLength, sharedBuffer, rleCtrlBuffer, ref rleCtrlIdx, rleCodeBuffer, ref rleCodeIdx); - } - - private static void TBytesDetermineRleType(ref RleRefClipStruct rleLoader, MemoryStream outCache, long copyLength, byte[] sharedBuffer, - ReadOnlySpan rleCtrlBuffer, ref int rleCtrlIdx, byte[] rleCodeBuffer, ref int rleCodeIdx) - { - TBytesSetRle(ref rleLoader, outCache, ref copyLength, sharedBuffer, rleCodeBuffer, ref rleCodeIdx); - - while (copyLength > 0) - { - byte pSign = rleCtrlBuffer[rleCtrlIdx++]; - byte type = (byte)(pSign >> (8 - PatchCore.KByteRleType)); - long length = rleCtrlBuffer.ReadLong7Bit(ref rleCtrlIdx, PatchCore.KByteRleType, pSign); - ++length; - - if (type == 3) - { - rleLoader.MemCopyLength = length; - TBytesSetRleCopyOnly(ref rleLoader, outCache, ref copyLength, sharedBuffer, rleCodeBuffer, ref rleCodeIdx); - continue; - } - - rleLoader.MemSetLength = length; - if (type == 2) - { - rleLoader.MemSetValue = rleCodeBuffer[rleCodeIdx++]; - TBytesSetRle(ref rleLoader, outCache, ref copyLength, sharedBuffer, rleCodeBuffer, ref rleCodeIdx); - continue; - } - - /* If the type is 1, then 0 - 1. This should result -1 in int but since - * we cast it to byte, then it underflow and set it to 255. - * This method is the same as: - * if (type == 0) - * rleLoader.memSetValue = 0x00; // or 0 in byte - * else - * rleLoader.memSetValue = 0xFF; // or 255 in byte - */ - rleLoader.MemSetValue = (byte)(0x00 - type); - TBytesSetRle(ref rleLoader, outCache, ref copyLength, sharedBuffer, rleCodeBuffer, ref rleCodeIdx); - } - } - - private static void TBytesSetRle(ref RleRefClipStruct rleLoader, MemoryStream outCache, ref long copyLength, byte[] sharedBuffer, - byte[] rleCodeBuffer, ref int rleCodeIdx) - { - PatchCore.TBytesSetRleSingle(ref rleLoader, outCache, ref copyLength, sharedBuffer); - - if (rleLoader.MemCopyLength == 0) return; - TBytesSetRleCopyOnly(ref rleLoader, outCache, ref copyLength, sharedBuffer, rleCodeBuffer, ref rleCodeIdx); - } - - private static unsafe void TBytesSetRleCopyOnly(ref RleRefClipStruct rleLoader, MemoryStream outCache, ref long copyLength, byte[] sharedBuffer, - byte[] rleCodeBuffer, ref int rleCodeIdx) - { - int decodeStep = (int)(rleLoader.MemCopyLength > copyLength ? copyLength : rleLoader.MemCopyLength); - - long lastPosCopy = outCache.Position; - _ = outCache.Read(sharedBuffer, 0, decodeStep); - outCache.Position = lastPosCopy; - - fixed (byte* rlePtr = &rleCodeBuffer[rleCodeIdx], oldPtr = &sharedBuffer[0]) - { - PatchCore.RleProcDelegate(ref rleLoader, outCache, ref copyLength, decodeStep, rlePtr, rleCodeBuffer, rleCodeIdx, oldPtr); - } - rleCodeIdx += decodeStep; - } - } -} diff --git a/SharpHDiffPatch.Core/Patch/PatchDir.cs b/SharpHDiffPatch.Core/Patch/PatchDir.cs deleted file mode 100644 index 0af3501..0000000 --- a/SharpHDiffPatch.Core/Patch/PatchDir.cs +++ /dev/null @@ -1,348 +0,0 @@ -using SharpHDiffPatch.Core.Binary; -using SharpHDiffPatch.Core.Binary.Compression; -using SharpHDiffPatch.Core.Binary.Streams; -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading; - -namespace SharpHDiffPatch.Core.Patch -{ - internal class PairIndexReference - { - public long OldIndex; - public long NewIndex; - - public override string ToString() => $"OldIndex: {OldIndex}, NewIndex: {NewIndex}"; - } - - internal class DirectoryReferencePair - { - internal string[] OldUtf8PathList; - internal string[] NewUtf8PathList; - internal long[] OldRefList; - internal long[] NewRefList; - internal long[] OldRefSizeList; - internal long[] NewRefSizeList; - internal long[] NewRefHashList; - internal PairIndexReference[] DataSamePairList; - internal long[] NewExecuteList; - } - - public sealed class PatchDir : IPatch - { - private HeaderInfo _headerInfo; - private readonly DataReferenceInfo _referenceInfo; - private readonly DirectoryPatchFormat _directoryPatchFormat; - private readonly Func _spawnPatchStream; - private string _basePathInput; - private string _basePathOutput; - private bool _useBufferedPatch; - private bool _useFullBuffer; - private bool _useFastBuffer; -#if USEEXPERIMENTALMULTITHREAD - private bool useMultiThread; -#endif - private int _padding; - private readonly CancellationToken _token; - - public PatchDir(HeaderInfo headerInfo, DataReferenceInfo referenceInfo, string patchPath, - DirectoryPatchFormat directoryPatchFormat, CancellationToken token -#if USEEXPERIMENTALMULTITHREAD - , bool useMultiThread -#endif - ) - { - _token = token; - _headerInfo = headerInfo; - _referenceInfo = referenceInfo; - _directoryPatchFormat = directoryPatchFormat; -#if USEEXPERIMENTALMULTITHREAD - useMultiThread = useMultiThread; -#endif - _spawnPatchStream = headerInfo.PatchCreateStream ?? (() => new FileStream(patchPath, FileMode.Open, FileAccess.Read, FileShare.Read)); - } - - public void Patch(string input, string output, Action writeBytesDelegate, bool useBufferedPatch, bool useFullBuffer, bool useFastBuffer) - { - _basePathInput = input; - _basePathOutput = output; - _useBufferedPatch = useBufferedPatch; - _useFullBuffer = useFullBuffer; - _useFastBuffer = useFastBuffer; - - using Stream patchStream = _spawnPatchStream(); - _padding = _headerInfo.CompMode == HDiffCompressionMode.zlib ? 1 : 0; - HDiffPatch.Event.PushLog($"[PatchDir::Patch] Padding applied: {_padding} byte(s)", Verbosity.Debug); - - int headerPadding = _referenceInfo.HeadDataCompressedSize > 0 ? _padding : 0; - patchStream.Position = _referenceInfo.HeadDataOffset + headerPadding; - HDiffPatch.Event.PushLog($"[PatchDir::Patch] Patch stream position moved from: {patchStream.Position - (_referenceInfo.HeadDataOffset + headerPadding)} to: {patchStream.Position}", Verbosity.Debug); - - HDiffPatch.Event.PushLog($"[PatchDir::Patch] Getting stream for header at size: {_referenceInfo.HeadDataSize} bytes ({_referenceInfo.HeadDataCompressedSize - headerPadding} bytes compressed)", Verbosity.Verbose); - - CompressionStreamHelper.GetDecompressStreamPlugin(_headerInfo.CompMode, patchStream, out Stream decompressedHeadStream, - _referenceInfo.HeadDataSize, _referenceInfo.HeadDataCompressedSize - headerPadding, out _, _useBufferedPatch); - - HDiffPatch.Event.PushLog("[PatchDir::Patch] Initializing stream to binary readers", Verbosity.Debug); - - using (patchStream) - using (decompressedHeadStream) - { - DirectoryReferencePair dirData = InitializeDirPatcher(decompressedHeadStream); - - long newPatchSize = GetNewPatchedFileSize(dirData); - long samePathSize = GetSameFileSize(dirData); - long totalSizePatched = newPatchSize + samePathSize; - - HDiffPatch.Event.PushLog($"[PatchDir::Patch] Total new size: {totalSizePatched} bytes ({newPatchSize} (new data) + {samePathSize} (same data))", Verbosity.Verbose); - - ValidateOldReferenceSizes(dirData); - FileStream[] mergedOldStream = GetRefOldStreams(dirData); - CombinedStreamSegment[] mergedNewStream = GetRefNewStreams(dirData); - HDiffPatch.Event.PushLog($"[PatchDir::Patch] Initialized {mergedOldStream.Length} old files and {mergedNewStream.Length} new files into combined stream", Verbosity.Verbose); - - HDiffPatch.Event.PushLog($"[PatchDir::Patch] Seek the patch stream to: {_referenceInfo.HDiffDataOffset}. Jump to read header for clip streams!", Verbosity.Verbose); - patchStream.Position = _referenceInfo.HDiffDataOffset; - if (!_headerInfo.IsSingleCompressedDiff) - _ = Header.TryParseHeaderInfo(patchStream, "", out _headerInfo, out _); - else - HDiffPatch.Event.PushLog("[PatchDir::Patch] This patch is a \"single diff\" type!"); - - _padding = _headerInfo.CompMode == HDiffCompressionMode.zlib ? 1 : 0; - - IPatchCore patchCore = CreatePatchCore(writeBytesDelegate, totalSizePatched); - patchCore.SetDirectoryReferencePair(dirData); - patchCore.SetSizeToBePatched(totalSizePatched); - - using (Stream newStream = new CombinedStream(mergedNewStream)) - using (Stream oldStream = new CombinedStream(mergedOldStream)) - { - long oldFileSize = GetOldFileSize(dirData); - if (oldStream.Length != _headerInfo.OldDataSize) - throw new InvalidDataException($"[PatchDir::Patch] The patch directory is expecting old size to be equivalent as: {_headerInfo.OldDataSize} bytes, but the input file has unmatch size: {oldStream.Length} bytes!"); - - HDiffPatch.Event.PushLog($"[PatchDir::Patch] Existing old directory size: {oldFileSize} is matched!", Verbosity.Verbose); - - long lastPos = patchStream.Position; - HDiffPatch.Event.PushLog($"[PatchDir::Patch] Staring patching routine at position: {lastPos}", Verbosity.Verbose); - - HDiffPatch.DisplayDirPatchInformation(oldFileSize, totalSizePatched, _headerInfo); - StartPatchRoutine(oldStream, newStream, _headerInfo.NewDataSize, lastPos, patchCore); - } - } - } - - private long GetOldFileSize(DirectoryReferencePair dirData) - { - long fileSize = 0; - for (int i = 0; i < dirData.OldUtf8PathList.Length; i++) - { - ref string basePath = ref dirData.OldUtf8PathList[i]; - if (basePath.Length == 0) continue; - - int baseIndexEoc = basePath.Length - 1; - char endOfChar = basePath[baseIndexEoc]; - if (endOfChar == '/') continue; - - string sourceFullPath = Path.Combine(_basePathInput, basePath); - if (!File.Exists(sourceFullPath)) continue; - - fileSize += new FileInfo(sourceFullPath).Length; - } - - return fileSize; - } - - private long GetSameFileSize(DirectoryReferencePair dirData) - { - long fileSize = 0; - foreach (PairIndexReference pair in dirData.DataSamePairList) - { - ref string basePath = ref dirData.NewUtf8PathList[pair.NewIndex]; - bool isPathADir = PatchCore.IsPathADir(basePath); - if (isPathADir) continue; - - string sourceFullPath = Path.Combine(_basePathInput, basePath); - if (!File.Exists(sourceFullPath)) continue; - - fileSize += new FileInfo(sourceFullPath).Length; - } - - return fileSize; - } - - private static long GetNewPatchedFileSize(DirectoryReferencePair dirData) => dirData.NewRefSizeList.Sum(); - - private void ValidateOldReferenceSizes(DirectoryReferencePair dirData) - { - if (_directoryPatchFormat != DirectoryPatchFormat.Kuro) - return; - - for (int i = 0; i < dirData.OldRefList.Length; i++) - { - ref string oldPath = ref PatchCore.NewPathByIndex(dirData.OldUtf8PathList, dirData.OldRefList[i]); - string fullPath = Path.Combine(_basePathInput, oldPath); - if (!File.Exists(fullPath)) - throw new FileNotFoundException("A Kuro directory patch source file was not found.", fullPath); - - long actualSize = new FileInfo(fullPath).Length; - long expectedSize = dirData.OldRefSizeList[i]; - if (actualSize != expectedSize) - throw new InvalidDataException($"[PatchDir::ValidateOldReferenceSizes] Source file size mismatch for {fullPath}: expected {expectedSize} bytes, got {actualSize} bytes."); - } - } - - private IPatchCore CreatePatchCore(Action writeBytesDelegate, long totalSizePatched) - { - bool wantFastBuffer = _useFastBuffer && _useBufferedPatch && !_headerInfo.IsSingleCompressedDiff; - switch (wantFastBuffer) - { - case true when PatchSizeHelper.CanUseFastBuffer(_headerInfo): - return new PatchCoreFastBuffer(totalSizePatched, Stopwatch.StartNew(), _basePathInput, _basePathOutput, writeBytesDelegate, _token); - case true: - HDiffPatch.Event.PushLog("[PatchDir::CreatePatchCore] Fast buffer disabled: patch chunk sizes exceed int32-safe limits; using streaming patch core."); - break; - } - - return new PatchCore(totalSizePatched, Stopwatch.StartNew(), _basePathInput, _basePathOutput, writeBytesDelegate, _token); - } - - private void StartPatchRoutine(Stream inputStream, Stream outputStream, long newDataSize, long offset, IPatchCore patchCore) - { - var clips = new Stream[_headerInfo.IsSingleCompressedDiff ? 1 : 4]; - Stream[] sourceClips = _headerInfo.IsSingleCompressedDiff ? - [ _spawnPatchStream() ] : - [ - _spawnPatchStream(), - _spawnPatchStream(), - _spawnPatchStream(), - _spawnPatchStream() - ]; - - try - { - if (_headerInfo.IsSingleCompressedDiff) - { - sourceClips[0].Position += _referenceInfo.HDiffDataOffset + _headerInfo.SingleChunkInfo.DiffDataPos; - int coverPadding = _headerInfo.SingleChunkInfo.CompressedSize > 0 ? _padding : 0; - offset += _headerInfo.SingleChunkInfo.DiffDataPos; - - clips[0] = patchCore.GetBufferStreamFromOffset(_headerInfo.CompMode, sourceClips[0], offset + coverPadding, - _headerInfo.SingleChunkInfo.UncompressedSize, _headerInfo.SingleChunkInfo.CompressedSize, out long _, - _useBufferedPatch, false); - } - else - { - int coverPadding = _headerInfo.ChunkInfo.CompressCoverBufSize > 0 ? _padding : 0; - clips[0] = patchCore.GetBufferStreamFromOffset(_headerInfo.CompMode, sourceClips[0], offset + coverPadding, - _headerInfo.ChunkInfo.CoverBufSize, _headerInfo.ChunkInfo.CompressCoverBufSize, out long nextLength, _useBufferedPatch, false); - - offset += nextLength; - int rleCtrlBufPadding = _headerInfo.ChunkInfo.CompressRleCtrlBufSize > 0 ? _padding : 0; - clips[1] = patchCore.GetBufferStreamFromOffset(_headerInfo.CompMode, sourceClips[1], offset + rleCtrlBufPadding, - _headerInfo.ChunkInfo.RleCtrlBufSize, _headerInfo.ChunkInfo.CompressRleCtrlBufSize, out nextLength, _useBufferedPatch, _useFastBuffer); - - offset += nextLength; - int rleCodeBufPadding = _headerInfo.ChunkInfo.CompressRleCodeBufSize > 0 ? _padding : 0; - clips[2] = patchCore.GetBufferStreamFromOffset(_headerInfo.CompMode, sourceClips[2], offset + rleCodeBufPadding, - _headerInfo.ChunkInfo.RleCodeBufSize, _headerInfo.ChunkInfo.CompressRleCodeBufSize, out nextLength, _useBufferedPatch, _useFastBuffer); - - offset += nextLength; - int newDataDiffPadding = _headerInfo.ChunkInfo.CompressNewDataDiffSize > 0 ? _padding : 0; - clips[3] = patchCore.GetBufferStreamFromOffset(_headerInfo.CompMode, sourceClips[3], offset + newDataDiffPadding, - _headerInfo.ChunkInfo.NewDataDiffSize, _headerInfo.ChunkInfo.CompressNewDataDiffSize - _padding, out _, _useBufferedPatch && _useFullBuffer, false); - - _headerInfo.NewDataSize = newDataSize; - } - patchCore.UncoverBufferClipsStream(clips, inputStream, outputStream, _headerInfo); - } - finally - { - foreach (Stream clip in clips) clip?.Dispose(); - foreach (Stream clip in sourceClips) clip?.Dispose(); - } - } - - private FileStream[] GetRefOldStreams(DirectoryReferencePair dirData) - { - FileStream[] streams = new FileStream[dirData.OldRefList.Length]; - for (int i = 0; i < dirData.OldRefList.Length; i++) - { - ref string oldPathByIndex = ref PatchCore.NewPathByIndex(dirData.OldUtf8PathList, dirData.OldRefList[i]); - string combinedOldPath = Path.Combine(_basePathInput, oldPathByIndex); - - HDiffPatch.Event.PushLog($"[PatchDir::GetRefOldStreams] Assigning stream to the old path: {combinedOldPath}", Verbosity.Debug); - streams[i] = File.Open(combinedOldPath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite); - } - - return streams; - } - - private CombinedStreamSegment[] GetRefNewStreams(DirectoryReferencePair dirData) - { - var streams = new CombinedStreamSegment[dirData.NewRefList.Length]; - for (int i = 0; i < dirData.NewRefList.Length; i++) - { - ref string newPathByIndex = ref PatchCore.NewPathByIndex(dirData.NewUtf8PathList, dirData.NewRefList[i]); - string combinedNewPath = Path.Combine(_basePathOutput, newPathByIndex); - string newPathDirectory = Path.GetDirectoryName(combinedNewPath); - - if (!string.IsNullOrEmpty(newPathDirectory)) - Directory.CreateDirectory(newPathDirectory); - - HDiffPatch.Event.PushLog($"[PatchDir::GetRefNewStreams] Assigning stream to the new path: {combinedNewPath}", Verbosity.Debug); - - var stream = new CombinedStreamSegment - { - Stream = new FileStream(combinedNewPath, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite), - Length = dirData.NewRefSizeList[i] - }; - streams[i] = stream; - } - - return streams; - } - - private DirectoryReferencePair InitializeDirPatcher(Stream reader) - { - HDiffPatch.Event.PushLog("[PatchDir::InitializeDirPatcher] Reading PatchDir header...", Verbosity.Verbose); - DirectoryReferencePair returnValue = new(); - - HDiffPatch.Event.PushLog($"[PatchDir::InitializeDirPatcher] Reading path string buffers -> OldPath: {(int)_referenceInfo.InputSumSize}", Verbosity.Verbose); - reader.GetPathsFromStream(out returnValue.OldUtf8PathList, (int)_referenceInfo.InputSumSize, (int)_referenceInfo.InputDirCount); - - HDiffPatch.Event.PushLog($"[PatchDir::InitializeDirPatcher] Reading path string buffers -> NewPath: {(int)_referenceInfo.OutputSumSize}", Verbosity.Verbose); - reader.GetPathsFromStream(out returnValue.NewUtf8PathList, (int)_referenceInfo.OutputSumSize, (int)_referenceInfo.OutputDirCount); - - HDiffPatch.Event.PushLog($"[PatchDir::InitializeDirPatcher] Path string counts -> OldPath: {returnValue.OldUtf8PathList.Length} paths & NewPath: {returnValue.NewUtf8PathList.Length} paths", Verbosity.Verbose); - reader.GetLongsFromStream(out returnValue.OldRefList, _referenceInfo.InputRefFileCount, _referenceInfo.InputDirCount); - reader.GetLongsFromStream(out returnValue.NewRefList, _referenceInfo.OutputRefFileCount, _referenceInfo.OutputDirCount); - if (_directoryPatchFormat == DirectoryPatchFormat.Kuro) - reader.GetLongsFromStream(out returnValue.OldRefSizeList, _referenceInfo.InputRefFileCount); - reader.GetLongsFromStream(out returnValue.NewRefSizeList, _referenceInfo.OutputRefFileCount); - if (_directoryPatchFormat == DirectoryPatchFormat.Kuro) - reader.GetLongsFromStream(out returnValue.NewRefHashList, _referenceInfo.OutputRefFileCount); - reader.GetPairIndexReferenceFromStream(out returnValue.DataSamePairList, _referenceInfo.SameFilePairCount, _referenceInfo.OutputDirCount, _referenceInfo.InputDirCount); - reader.GetLongsFromStream(out returnValue.NewExecuteList, _referenceInfo.NewExecuteCount, _referenceInfo.OutputDirCount); - - if (_directoryPatchFormat == DirectoryPatchFormat.Kuro) - { - long oldRefSize = returnValue.OldRefSizeList.Sum(); - if (oldRefSize != _referenceInfo.InputRefFileSize) - throw new InvalidDataException($"[PatchDir::InitializeDirPatcher] Kuro old reference size mismatch: expected {_referenceInfo.InputRefFileSize} bytes, parsed {oldRefSize} bytes."); - - long newRefSize = returnValue.NewRefSizeList.Sum(); - if (newRefSize != _referenceInfo.OutputRefFileSize) - throw new InvalidDataException($"[PatchDir::InitializeDirPatcher] Kuro new reference size mismatch: expected {_referenceInfo.OutputRefFileSize} bytes, parsed {newRefSize} bytes."); - - if (reader.CanSeek && reader.Length - reader.Position != _referenceInfo.PrivateReservedDataSize) - throw new InvalidDataException($"[PatchDir::InitializeDirPatcher] Directory header has {reader.Length - reader.Position} unparsed bytes; expected {_referenceInfo.PrivateReservedDataSize} private reserved bytes."); - } - HDiffPatch.Event.PushLog($"[PatchDir::InitializeDirPatcher] Path refs found! OldRef: {_referenceInfo.InputRefFileCount} paths, NewRef: {_referenceInfo.OutputRefFileCount} paths, IdenticalRef: {_referenceInfo.SameFilePairCount} paths", Verbosity.Verbose); - - return returnValue; - } - } -} diff --git a/SharpHDiffPatch.Core/Patch/PatchSingle.cs b/SharpHDiffPatch.Core/Patch/PatchSingle.cs deleted file mode 100644 index 34ef334..0000000 --- a/SharpHDiffPatch.Core/Patch/PatchSingle.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Threading; -using SharpHDiffPatch.Core.Binary; -using SharpHDiffPatch.Core.Binary.Compression; - -namespace SharpHDiffPatch.Core.Patch -{ - public sealed class PatchSingle(HeaderInfo headerInfo, CancellationToken token) : IPatch - { - private readonly Func _spawnPatchStream = headerInfo.PatchCreateStream ?? (() => new FileStream(headerInfo.PatchPath, FileMode.Open, FileAccess.Read, FileShare.Read)); - - private bool _isUseBufferedPatch; - private bool _isUseFullBuffer; - private bool _isUseFastBuffer; - - public void Patch(string input, string output, Action writeBytesDelegate, bool useBufferedPatch, bool useFullBuffer, bool useFastBuffer) - { - _isUseBufferedPatch = useBufferedPatch; - _isUseFullBuffer = useFullBuffer; - _isUseFastBuffer = useFastBuffer; - - using FileStream inputStream = new(input, FileMode.Open, FileAccess.Read, FileShare.Read, headerInfo.OldDataSize.GetFileStreamBufferSize()); - using FileStream outputStream = new(output, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite, headerInfo.NewDataSize.GetFileStreamBufferSize()); - if (inputStream.Length != headerInfo.OldDataSize) - throw new InvalidDataException($"[PatchSingle::Patch] The patch directory is expecting old size to be equivalent as: {headerInfo.OldDataSize} bytes, but the input file has unmatched size: {inputStream.Length} bytes!"); - - HDiffPatch.Event.PushLog($"[PatchSingle::Patch] Existing old file size: {inputStream.Length} is matched!", Verbosity.Verbose); - HDiffPatch.Event.PushLog($"[PatchSingle::Patch] Staring patching routine at position: {headerInfo.ChunkInfo.HeadEndPos}", Verbosity.Verbose); - - IPatchCore patchCore = CreatePatchCore(input, output, writeBytesDelegate); - - StartPatchRoutine(inputStream, outputStream, patchCore); - } - - private IPatchCore CreatePatchCore(string input, string output, Action writeBytesDelegate) - { - bool wantFastBuffer = _isUseFastBuffer && _isUseBufferedPatch; - if (wantFastBuffer && PatchSizeHelper.CanUseFastBuffer(headerInfo)) - return new PatchCoreFastBuffer(headerInfo.NewDataSize, Stopwatch.StartNew(), input, output, writeBytesDelegate, token); - - if (wantFastBuffer) - HDiffPatch.Event.PushLog("[PatchSingle::CreatePatchCore] Fast buffer disabled: patch chunk sizes exceed int32-safe limits; using streaming patch core."); - - return new PatchCore(headerInfo.NewDataSize, Stopwatch.StartNew(), input, output, writeBytesDelegate, token); - } - - private void StartPatchRoutine(Stream inputStream, Stream outputStream, IPatchCore patchCore) - { - Stream[] clips = new Stream[4]; - Stream[] sourceClips = - [ - _spawnPatchStream(), - _spawnPatchStream(), - _spawnPatchStream(), - _spawnPatchStream() - ]; - - int padding = headerInfo.CompMode == HDiffCompressionMode.zlib ? 1 : 0; - - try - { - long offset = headerInfo.ChunkInfo.HeadEndPos; - int coverPadding = headerInfo.ChunkInfo.CompressCoverBufSize > 0 ? padding : 0; - clips[0] = patchCore.GetBufferStreamFromOffset(headerInfo.CompMode, sourceClips[0], offset + coverPadding, - headerInfo.ChunkInfo.CoverBufSize, headerInfo.ChunkInfo.CompressCoverBufSize, out long nextLength, _isUseBufferedPatch, false); - - offset += nextLength; - int rleCtrlBufPadding = headerInfo.ChunkInfo.CompressRleCtrlBufSize > 0 ? padding : 0; - clips[1] = patchCore.GetBufferStreamFromOffset(headerInfo.CompMode, sourceClips[1], offset + rleCtrlBufPadding, - headerInfo.ChunkInfo.RleCtrlBufSize, headerInfo.ChunkInfo.CompressRleCtrlBufSize, out nextLength, _isUseBufferedPatch, _isUseFastBuffer); - - offset += nextLength; - int rleCodeBufPadding = headerInfo.ChunkInfo.CompressRleCodeBufSize > 0 ? padding : 0; - clips[2] = patchCore.GetBufferStreamFromOffset(headerInfo.CompMode, sourceClips[2], offset + rleCodeBufPadding, - headerInfo.ChunkInfo.RleCodeBufSize, headerInfo.ChunkInfo.CompressRleCodeBufSize, out nextLength, _isUseBufferedPatch, _isUseFastBuffer); - - offset += nextLength; - int newDataDiffPadding = headerInfo.ChunkInfo.CompressNewDataDiffSize > 0 ? padding : 0; - clips[3] = patchCore.GetBufferStreamFromOffset(headerInfo.CompMode, sourceClips[3], offset + newDataDiffPadding, - headerInfo.ChunkInfo.NewDataDiffSize, headerInfo.ChunkInfo.CompressNewDataDiffSize - padding, out _, _isUseBufferedPatch && _isUseFullBuffer, false); - - patchCore.UncoverBufferClipsStream(clips, inputStream, outputStream, headerInfo); - } - finally - { - foreach (Stream clip in clips) clip?.Dispose(); - foreach (Stream clip in sourceClips) clip?.Dispose(); - } - } - } -} diff --git a/SharpHDiffPatch.Core/Patch/PatchSizeHelper.cs b/SharpHDiffPatch.Core/Patch/PatchSizeHelper.cs deleted file mode 100644 index b8b3836..0000000 --- a/SharpHDiffPatch.Core/Patch/PatchSizeHelper.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; - -namespace SharpHDiffPatch.Core.Patch -{ - internal static class PatchSizeHelper - { - internal static bool FitsInInt32(long value) => value is >= 0 and <= int.MaxValue; - - internal static int ToCheckedInt32(long value, string paramName) - { - if (!FitsInInt32(value)) - throw new ArgumentOutOfRangeException(paramName, value, - $"Value exceeds maximum safe array length ({int.MaxValue})."); - - return (int)value; - } - - /// - /// Returns true when can allocate its working - /// buffers without overflowing -sized APIs (ArrayPool, byte[], etc.). - /// - internal static bool CanUseFastBuffer(HeaderInfo headerInfo) - { - if (headerInfo.IsSingleCompressedDiff) - return false; - - DiffChunkInfo chunk = headerInfo.ChunkInfo; - - if (!FitsInInt32(chunk.RleCtrlBufSize)) - return false; - - if (!FitsInInt32(chunk.RleCodeBufSize)) - return false; - - if (!FitsInInt32(chunk.CoverBufSize)) - return false; - - long allBufferSize = chunk.RleCtrlBufSize + chunk.RleCodeBufSize + chunk.CoverBufSize; - return IsMemorySufficient(allBufferSize); - } - - private static bool IsMemorySufficient(long bufferSize) - { -#if NET6_0_OR_GREATER - GCMemoryInfo info = GC.GetGCMemoryInfo(); - - // Get possible minimum free memory size. - // Let's say for a mid-end device with low memory capacity: - // Free Mem: 2 GiB * 0.50 = 1 GiB - // Divided by CPU threads: 1 GiB / 8 = 128 MiB - long thresholdPossibleFreeMemSize = (long)(info.TotalAvailableMemoryBytes * 0.50d / Environment.ProcessorCount); - return thresholdPossibleFreeMemSize >= bufferSize; -#else - return true; // We have no simple way to get memory info. So, just pass it in. -#endif - } - } -} diff --git a/SharpHDiffPatch.sln b/SharpHDiffPatch.sln index 5dd2029..c13cced 100644 --- a/SharpHDiffPatch.sln +++ b/SharpHDiffPatch.sln @@ -3,9 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 VisualStudioVersion = 18.7.11925.98 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpHDiffPatch.Core", "SharpHDiffPatch.Core\SharpHDiffPatch.Core.csproj", "{57CB7A6D-2474-4A01-BE1A-5D1488F81390}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpHPatchZ", "SharpHPatchZ\SharpHPatchZ.csproj", "{7CE06EE4-B691-464B-9AB1-9882CF83E48E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpHDiffPatch", "SharpHDiffPatch\SharpHDiffPatch.csproj", "{506DDD1C-DD74-4FCE-A0C9-D90E904CF708}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpHPatchZ.Program", "SharpHPatchZ.Program\SharpHPatchZ.Program.csproj", "{BD9154E6-EAF5-5C39-79A2-690F8CBED254}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -19,38 +19,38 @@ Global Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {57CB7A6D-2474-4A01-BE1A-5D1488F81390}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {57CB7A6D-2474-4A01-BE1A-5D1488F81390}.Debug|Any CPU.Build.0 = Debug|Any CPU - {57CB7A6D-2474-4A01-BE1A-5D1488F81390}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {57CB7A6D-2474-4A01-BE1A-5D1488F81390}.Debug|ARM64.Build.0 = Debug|ARM64 - {57CB7A6D-2474-4A01-BE1A-5D1488F81390}.Debug|x64.ActiveCfg = Debug|x64 - {57CB7A6D-2474-4A01-BE1A-5D1488F81390}.Debug|x64.Build.0 = Debug|x64 - {57CB7A6D-2474-4A01-BE1A-5D1488F81390}.Debug|x86.ActiveCfg = Debug|x86 - {57CB7A6D-2474-4A01-BE1A-5D1488F81390}.Debug|x86.Build.0 = Debug|x86 - {57CB7A6D-2474-4A01-BE1A-5D1488F81390}.Release|Any CPU.ActiveCfg = Release|Any CPU - {57CB7A6D-2474-4A01-BE1A-5D1488F81390}.Release|Any CPU.Build.0 = Release|Any CPU - {57CB7A6D-2474-4A01-BE1A-5D1488F81390}.Release|ARM64.ActiveCfg = Release|ARM64 - {57CB7A6D-2474-4A01-BE1A-5D1488F81390}.Release|ARM64.Build.0 = Release|ARM64 - {57CB7A6D-2474-4A01-BE1A-5D1488F81390}.Release|x64.ActiveCfg = Release|x64 - {57CB7A6D-2474-4A01-BE1A-5D1488F81390}.Release|x64.Build.0 = Release|x64 - {57CB7A6D-2474-4A01-BE1A-5D1488F81390}.Release|x86.ActiveCfg = Release|x86 - {57CB7A6D-2474-4A01-BE1A-5D1488F81390}.Release|x86.Build.0 = Release|x86 - {506DDD1C-DD74-4FCE-A0C9-D90E904CF708}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {506DDD1C-DD74-4FCE-A0C9-D90E904CF708}.Debug|Any CPU.Build.0 = Debug|Any CPU - {506DDD1C-DD74-4FCE-A0C9-D90E904CF708}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {506DDD1C-DD74-4FCE-A0C9-D90E904CF708}.Debug|ARM64.Build.0 = Debug|ARM64 - {506DDD1C-DD74-4FCE-A0C9-D90E904CF708}.Debug|x64.ActiveCfg = Debug|x64 - {506DDD1C-DD74-4FCE-A0C9-D90E904CF708}.Debug|x64.Build.0 = Debug|x64 - {506DDD1C-DD74-4FCE-A0C9-D90E904CF708}.Debug|x86.ActiveCfg = Debug|x86 - {506DDD1C-DD74-4FCE-A0C9-D90E904CF708}.Debug|x86.Build.0 = Debug|x86 - {506DDD1C-DD74-4FCE-A0C9-D90E904CF708}.Release|Any CPU.ActiveCfg = Release|Any CPU - {506DDD1C-DD74-4FCE-A0C9-D90E904CF708}.Release|Any CPU.Build.0 = Release|Any CPU - {506DDD1C-DD74-4FCE-A0C9-D90E904CF708}.Release|ARM64.ActiveCfg = Release|ARM64 - {506DDD1C-DD74-4FCE-A0C9-D90E904CF708}.Release|ARM64.Build.0 = Release|ARM64 - {506DDD1C-DD74-4FCE-A0C9-D90E904CF708}.Release|x64.ActiveCfg = Release|x64 - {506DDD1C-DD74-4FCE-A0C9-D90E904CF708}.Release|x64.Build.0 = Release|x64 - {506DDD1C-DD74-4FCE-A0C9-D90E904CF708}.Release|x86.ActiveCfg = Release|x86 - {506DDD1C-DD74-4FCE-A0C9-D90E904CF708}.Release|x86.Build.0 = Release|x86 + {7CE06EE4-B691-464B-9AB1-9882CF83E48E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7CE06EE4-B691-464B-9AB1-9882CF83E48E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7CE06EE4-B691-464B-9AB1-9882CF83E48E}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {7CE06EE4-B691-464B-9AB1-9882CF83E48E}.Debug|ARM64.Build.0 = Debug|ARM64 + {7CE06EE4-B691-464B-9AB1-9882CF83E48E}.Debug|x64.ActiveCfg = Debug|x64 + {7CE06EE4-B691-464B-9AB1-9882CF83E48E}.Debug|x64.Build.0 = Debug|x64 + {7CE06EE4-B691-464B-9AB1-9882CF83E48E}.Debug|x86.ActiveCfg = Debug|x86 + {7CE06EE4-B691-464B-9AB1-9882CF83E48E}.Debug|x86.Build.0 = Debug|x86 + {7CE06EE4-B691-464B-9AB1-9882CF83E48E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7CE06EE4-B691-464B-9AB1-9882CF83E48E}.Release|Any CPU.Build.0 = Release|Any CPU + {7CE06EE4-B691-464B-9AB1-9882CF83E48E}.Release|ARM64.ActiveCfg = Release|ARM64 + {7CE06EE4-B691-464B-9AB1-9882CF83E48E}.Release|ARM64.Build.0 = Release|ARM64 + {7CE06EE4-B691-464B-9AB1-9882CF83E48E}.Release|x64.ActiveCfg = Release|x64 + {7CE06EE4-B691-464B-9AB1-9882CF83E48E}.Release|x64.Build.0 = Release|x64 + {7CE06EE4-B691-464B-9AB1-9882CF83E48E}.Release|x86.ActiveCfg = Release|x86 + {7CE06EE4-B691-464B-9AB1-9882CF83E48E}.Release|x86.Build.0 = Release|x86 + {BD9154E6-EAF5-5C39-79A2-690F8CBED254}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BD9154E6-EAF5-5C39-79A2-690F8CBED254}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BD9154E6-EAF5-5C39-79A2-690F8CBED254}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {BD9154E6-EAF5-5C39-79A2-690F8CBED254}.Debug|ARM64.Build.0 = Debug|ARM64 + {BD9154E6-EAF5-5C39-79A2-690F8CBED254}.Debug|x64.ActiveCfg = Debug|x64 + {BD9154E6-EAF5-5C39-79A2-690F8CBED254}.Debug|x64.Build.0 = Debug|x64 + {BD9154E6-EAF5-5C39-79A2-690F8CBED254}.Debug|x86.ActiveCfg = Debug|x86 + {BD9154E6-EAF5-5C39-79A2-690F8CBED254}.Debug|x86.Build.0 = Debug|x86 + {BD9154E6-EAF5-5C39-79A2-690F8CBED254}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BD9154E6-EAF5-5C39-79A2-690F8CBED254}.Release|Any CPU.Build.0 = Release|Any CPU + {BD9154E6-EAF5-5C39-79A2-690F8CBED254}.Release|ARM64.ActiveCfg = Release|ARM64 + {BD9154E6-EAF5-5C39-79A2-690F8CBED254}.Release|ARM64.Build.0 = Release|ARM64 + {BD9154E6-EAF5-5C39-79A2-690F8CBED254}.Release|x64.ActiveCfg = Release|x64 + {BD9154E6-EAF5-5C39-79A2-690F8CBED254}.Release|x64.Build.0 = Release|x64 + {BD9154E6-EAF5-5C39-79A2-690F8CBED254}.Release|x86.ActiveCfg = Release|x86 + {BD9154E6-EAF5-5C39-79A2-690F8CBED254}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/SharpHDiffPatch/Program.cs b/SharpHDiffPatch/Program.cs deleted file mode 100644 index 1d8b68d..0000000 --- a/SharpHDiffPatch/Program.cs +++ /dev/null @@ -1,211 +0,0 @@ -using System; -using System.CommandLine; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -using SharpHDiffPatch.Core; -using SharpHDiffPatch.Core.Event; -#if BENCHMARK -using System.Linq; -#endif -// ReSharper disable CommentTypo - -namespace SharpHDiffPatch -{ - public static class PatcherBin - { - private static readonly string[] SizeSuffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; - private static readonly Stopwatch RefreshStopwatch = Stopwatch.StartNew(); - private const int RefreshInterval = 100; - private static readonly RootCommand Command = new RootCommand(); - - public static int Main(params string[] args) - { - Argument inputPathArg, patchPathArg, outputPathArg; - Option bufferModeOpt; - Option bufferFastOpt; - // Option _multiThreadOpt; - Option logLevelOpt; - - string inputPath, patchPath, outputPath; - bool isUseBufferedPatch, isUseFullBuffer, isUseFastBuffer - //, isUseMultiThread - ; - - Command.AddArgument(inputPathArg = new Argument("Input File", "Input path of the old file/folder to patch")); - Command.AddArgument(patchPathArg = new Argument("Patch File", "Patch file path to produce the new version of the file/folder")); - Command.AddArgument(outputPathArg = new Argument("Output File", "Output path of the new version to be produced")); - Command.AddOption(bufferModeOpt = new Option(["-b", "--buffer-mode"], () => BufferMode.Partial, - """ - Determines the buffering mode for reading the clips of the patch files. - [None] - No buffering and read the clips directly from the disk stream - - [Partial] - Buffers only the Cover Code, RLE Control and RLE Code clips only into memory. Read the New Data clip directly from the disk stream. - - [Full] - Buffers all clips into memory. This option is the fastest but it requires more memory depending on the patch size. - """)); - Command.AddOption(bufferFastOpt = new Option(["-B", "--fast-buffer"], () => false, "Use array-based buffer for RLE Control and Code clips.")); - Command.AddOption(logLevelOpt = new Option(["-l", "--log-level"], () => Verbosity.Info, "Defines the verbosity of the info to be displayed.")); - - Command.SetHandler((context) => - { - inputPath = context.ParseResult.GetValueForArgument(inputPathArg); - patchPath = context.ParseResult.GetValueForArgument(patchPathArg); - outputPath = context.ParseResult.GetValueForArgument(outputPathArg); - - (isUseBufferedPatch, isUseFullBuffer) = context.ParseResult.GetValueForOption(bufferModeOpt) switch - { - BufferMode.Full => (true, true), - BufferMode.Partial => (true, false), - _ => (false, false) - }; - - isUseFastBuffer = context.ParseResult.GetValueForOption(bufferFastOpt); - // isUseMultiThread = context.ParseResult.GetValueForOption(_multiThreadOpt); - HDiffPatch.LogVerbosity = context.ParseResult.GetValueForOption(logLevelOpt); - - try - { - HDiffPatch patcher = new HDiffPatch(); - if (HDiffPatch.LogVerbosity != Verbosity.Quiet) - { - EventListener.LoggerEvent += EventListener_LoggerEvent; - EventListener.PatchEvent += EventListener_PatchEvent; - } -#if BENCHMARK - Stopwatch benchmarkSw = Stopwatch.StartNew(); - double[] warmUpAvgs = new double[5]; - Console.WriteLine($"Warming up {warmUpAvgs.Length} runtime attempts!"); - for (int h = 0; h < warmUpAvgs.Length; h++) - { - patcher.Initialize(patchPath); - patcher.Patch(inputPath, outputPath, isUseBufferedPatch, default, isUseFullBuffer, isUseFastBuffer); - warmUpAvgs[h] = benchmarkSw?.Elapsed.TotalMilliseconds ?? 0; - benchmarkSw?.Restart(); - Console.WriteLine($" Starting warm-up {h + 1}: {warmUpAvgs[h]} ms"); - } - Console.WriteLine($"Finished warming up runtime attempts in: {warmUpAvgs.Average()} ms"); - - double[] numAvgs = new double[5]; - Console.WriteLine($"Starting all {numAvgs.Length} runtime attempts!"); - for (int h = 0; h < numAvgs.Length; h++) - { - Console.WriteLine($" Starting Attempt {h + 1}: {numAvgs[h]} ms"); - int repeat = 20; - double[] num = new double[repeat]; - benchmarkSw?.Restart(); - for (int i = 0; i < num.Length; i++) - { -#endif - patcher.Initialize(patchPath); -#if !BENCHMARK - RefreshStopwatch?.Restart(); -#endif - patcher.Patch(inputPath, outputPath, isUseBufferedPatch, CancellationToken.None, isUseFullBuffer, isUseFastBuffer - // , isUseMultiThread - ); -#if BENCHMARK - num[i] = benchmarkSw?.Elapsed.TotalMilliseconds ?? 0; - benchmarkSw?.Restart(); - Console.WriteLine($" Finished on run {i + 1} - {repeat} Attempt {h + 1}: {num[i]} ms"); - } - - numAvgs[h] = num.Average(); - Console.WriteLine($" Runtime Attempt {h + 1}: {numAvgs[h]} ms"); - } - Console.WriteLine($"Average all {numAvgs.Length} runtime attempt: {numAvgs.Average()} ms"); -#endif - } - catch (Exception ex) - { - Console.WriteLine($"An error has occurred! [{ex.GetType().Name}]: {ex.Message}\r\nStack Trace:\r\n{ex.StackTrace}"); - context.ExitCode = int.MinValue; -#if DEBUG - throw; -#endif - } - finally - { - RefreshStopwatch?.Stop(); - if (HDiffPatch.LogVerbosity != Verbosity.Quiet) - { - EventListener.LoggerEvent -= EventListener_LoggerEvent; - EventListener.PatchEvent -= EventListener_PatchEvent; - } - } - }); - - return Command.Invoke(args); - } - - private static void EventListener_LoggerEvent(object? sender, LoggerEvent e) - { - if (HDiffPatch.LogVerbosity == Verbosity.Quiet - || (HDiffPatch.LogVerbosity == Verbosity.Debug - && e.LogLevel is not (Verbosity.Debug or Verbosity.Verbose or Verbosity.Info)) - || (HDiffPatch.LogVerbosity == Verbosity.Verbose - && e.LogLevel is not (Verbosity.Verbose or Verbosity.Info)) - || (HDiffPatch.LogVerbosity == Verbosity.Info - && e.LogLevel != Verbosity.Info)) return; - - PrintLog(e); - } - - private static void PrintLog(LoggerEvent e) - { - string label = e.LogLevel switch - { - Verbosity.Info => "[Info] ", - Verbosity.Verbose => "[Verbose] ", - Verbosity.Debug => "[Debug] ", - _ => "" - }; - - Console.WriteLine($"{label}{e.Message}"); - } - - private static async void EventListener_PatchEvent(object? sender, PatchEvent e) - { - try - { - if (await CheckIfNeedRefreshStopwatch()) - { - Console.Write( - $"Patching: {e.ProgressPercentage}% | " + - $"{SummarizeSizeSimple(e.CurrentSizePatched)}/{SummarizeSizeSimple(e.TotalSizeToBePatched)} " + - $"@{SummarizeSizeSimple(e.Speed)}/s " + - $"[{string.Format("{0:hh}h:{0:mm}m:{0:ss}s remaining", TimeSpan.FromSeconds((e.TotalSizeToBePatched - e.CurrentSizePatched) / UnZeroed(e.Speed)))}] \r"); - } - - return; - static double UnZeroed(double input) => Math.Max(input, 1); - } - catch - { - // ignored - } - } - - private static async Task CheckIfNeedRefreshStopwatch() - { - if (RefreshStopwatch.ElapsedMilliseconds > RefreshInterval) - { - RefreshStopwatch.Restart(); - return true; - } - - await Task.Delay(RefreshInterval); - return false; - } - - private static string SummarizeSizeSimple(double value, int decimalPlaces = 2) - { - byte mag = (byte)Math.Log(value, 1000); - - return $"{Math.Round(value / (1L << (mag * 10)), decimalPlaces)} {SizeSuffixes[mag]}"; - } - } -} diff --git a/SharpHDiffPatch/Properties/launchSettings.json b/SharpHDiffPatch/Properties/launchSettings.json deleted file mode 100644 index 365f845..0000000 --- a/SharpHDiffPatch/Properties/launchSettings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "profiles": { - "SharpHDiffPatch-bin": { - "commandName": "Project", - "commandLineArgs": "\"G:\\Hi3SEA\" \"G:\\Hi3CNRecipe.cookbook.bz2\" \"K:\\Hi3CN-dotnet\" -l Verbose -B -b partial" - } - } -} \ No newline at end of file diff --git a/SharpHDiffPatch/SharpHDiffPatch.csproj b/SharpHDiffPatch/SharpHDiffPatch.csproj deleted file mode 100644 index d9b35e0..0000000 --- a/SharpHDiffPatch/SharpHDiffPatch.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - Exe - net10.0 - SharpHDiffPatch - disable - enable - x64;ARM64;AnyCPU;x86 - 2.3.4 - true - Debug;Release - - - - - - diff --git a/SharpHPatchZ.Program/Program.cs b/SharpHPatchZ.Program/Program.cs new file mode 100644 index 0000000..7f38349 --- /dev/null +++ b/SharpHPatchZ.Program/Program.cs @@ -0,0 +1,508 @@ +using SharpHPatchZ.Header; +using SharpHPatchZ.Header.Metadata; +using System; +using System.CommandLine; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading; + +#if NET6_0_OR_GREATER +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics.X86; +#endif + +// ReSharper disable InconsistentNaming +// ReSharper disable CommentTypo + +namespace SharpHPatchZ.Program; + +public static class PatcherBin +{ + private static readonly string[] SizeSuffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + private const int RefreshInterval = 100; + private static readonly RootCommand Command = new(); + private static readonly Stopwatch Stopwatch = Stopwatch.StartNew(); + private static readonly Stopwatch RefreshStopwatch = Stopwatch.StartNew(); + + public enum PatchPreset + { + Performance, + Balanced, + MemoryOptimized, + OptimizedForHDD + } + + private static Argument RegisterTo(this Argument argument, Command command) + { + command.AddArgument(argument); + return argument; + } + + private static Option RegisterTo(this Option option, Command command) + { + command.AddOption(option); + return option; + } + + private static void AddCommandPatch(RootCommand command) + { + Command patchCommand = new("patch", "Perform patch using provided Input, Patch and Output files"); + command.AddCommand(patchCommand); + + Argument inputPathArg = new Argument("Input File", "Input path of the old file/folder to patch").RegisterTo(patchCommand); + Argument patchPathArg = new Argument("Patch File", "Patch file path to produce the new version of the file/folder").RegisterTo(patchCommand); + Argument outputPathArg = new Argument("Output File", "Output path of the new version to be produced").RegisterTo(patchCommand); + + Option bufferModeOpt = new Option(["-p", "--preset"], () => PatchPreset.Performance, + """ + Determines the preset of the patching mode and also set its buffer size. + [Performance] + Buffer Size = Up to 16 MB per chunks + Threads = Automatic + + [Balanced] + Buffer Size = Up to 1 MB per chunks + Threads = Automatic + + [MemoryOptimized] + Buffer Size = Up to 128 KB per chunks + Threads = Automatic + + [OptimizedForHDD] + Buffer Size = Up to 16 MB per chunks + Threads = 1 + (Note: -p/--parallel-threads will be ignored with this mode) + + + """).RegisterTo(patchCommand); + + Option threadsOpt = new Option(["-t", "--parallel-threads"], () => (uint)Environment.ProcessorCount, + "Determines how much parallel threads to be run.").RegisterTo(patchCommand); + Option isKuroTypeOpt = new Option(["-k", "--kuro"], + "Defining that the patch file is a Kuro Games HDiff format.").RegisterTo(patchCommand); + +#if NET6_0_OR_GREATER + Option useSimdOpt = new Option(["--simd"], + () => true, + "Whether to use SIMD calculation while performing RLE additions.").RegisterTo(patchCommand); +#endif + + Option bufferCopyBufferSizeOpt = new Option(["--buffer-copy"], + "Determines how big the buffer size while performing copy routines to similar files.").RegisterTo(patchCommand); + Option bufferPatchWorkerBufferSizeOpt = new Option(["--buffer-patch"], + "Determines how big the buffer size used by the patch worker.").RegisterTo(patchCommand); + Option bufferReaderBufferSizeOpt = new Option(["--buffer-reader"], + "Determines how big the buffer size used by the RLE Stream Reader.").RegisterTo(patchCommand); + + patchCommand.SetHandler(context => + { + string inputPath = context.ParseResult.GetValueForArgument(inputPathArg); + string patchPath = context.ParseResult.GetValueForArgument(patchPathArg); + string outputPath = context.ParseResult.GetValueForArgument(outputPathArg); + bool isKuroType = context.ParseResult.GetValueForOption(isKuroTypeOpt); + uint threads = context.ParseResult.GetValueForOption(threadsOpt); + +#if NET6_0_OR_GREATER + bool useSimd = context.ParseResult.GetValueForOption(useSimdOpt); +#endif + + uint bufferCopyBufferSize = context.ParseResult.GetValueForOption(bufferCopyBufferSizeOpt); + uint bufferPatchWorkerBufferSize = context.ParseResult.GetValueForOption(bufferPatchWorkerBufferSizeOpt); + uint bufferReaderBufferSize = context.ParseResult.GetValueForOption(bufferReaderBufferSizeOpt); + + PatchPreset patchPreset = context.ParseResult.GetValueForOption(bufferModeOpt); + PatchOptions options = patchPreset switch + { + PatchPreset.Performance => PatchOptions.BigBuffer, + PatchPreset.Balanced => PatchOptions.Default, + PatchPreset.MemoryOptimized => PatchOptions.SmallBuffer, + PatchPreset.OptimizedForHDD => PatchOptions.OptimizeForHDD, + _ => PatchOptions.BigBuffer + }; + + if (bufferCopyBufferSize != 0) options.CopyBufferSize = (int)bufferCopyBufferSize; + if (bufferPatchWorkerBufferSize != 0) options.PatchWorkerBufferSize = (int)bufferPatchWorkerBufferSize; + if (bufferReaderBufferSize != 0) options.ReaderBufferSize = (int)bufferReaderBufferSize; + + InitializeOptions initializeOptions = new() + { + IsKuroGamesHDiff = isKuroType + }; + +#if NET6_0_OR_GREATER + if (useSimd) options.UseSIMD = useSimd; +#endif + + if (patchPreset != PatchPreset.OptimizedForHDD) + { + threads = context.ParseResult.GetValueForOption(threadsOpt); + options = options with + { + ParallelThreads = threads + }; + } + + try + { + ProgressCallback progressCallback = ProgressCallback.CreateFromManaged(PatchProgress); + + using HDiffInfo info = HPatch.CreateInstance(pos => CreatePatchStream(patchPath, pos), initializeOptions); + PrintFileInfo(info, inputPath, patchPath, outputPath, options, patchPreset); + PatchResult result = HPatch.Patch(info, pos => CreatePatchStream(patchPath, pos), inputPath, outputPath, options, progressCallback); + Console.WriteLine(); + + if (result.Exception != null) + { + Console.WriteLine($"Patching process throws an error: {result.Exception}"); + context.ExitCode = result; + return; + } + Console.WriteLine($"Patch completed in: {Stopwatch.Elapsed:c} ({Stopwatch.Elapsed.TotalSeconds} seconds)"); + } + catch (Exception ex) + { + Console.WriteLine($"An error has occurred! [{ex.GetType().Name}]: {ex.Message}\r\nStack Trace:\r\n{ex.StackTrace}"); + context.ExitCode = int.MinValue; +#if DEBUG + throw; +#endif + } + finally + { + Stopwatch.Stop(); + RefreshStopwatch.Stop(); + } + }); + } + + private static (Stream, bool) CreatePatchStream(string patchPath, long position) + { + FileStream stream = File.Open(patchPath, FileMode.Open, FileAccess.Read, FileShare.Read); + stream.Position = position; + return (stream, false); + } + + private static void AddCommandInfo(RootCommand command) + { + Command infoCommand = new("info", "Prints information about the patch file"); + command.AddCommand(infoCommand); + + Argument patchFileArg = new Argument("Patch File", "The path of the patch file in which to get the info from.").RegisterTo(infoCommand); + + Option verbosePrint = new Option("--verbose", + () => false, + "Prints more verbose information about the patch file. If the patch file is a directory patch, this will print the reference file paths as well.") + .RegisterTo(infoCommand); + + infoCommand.SetHandler(context => + { + string patchFilePath = context.ParseResult.GetValueForArgument(patchFileArg); + bool isVerbosePrint = context.ParseResult.GetValueForOption(verbosePrint); + + HDiffInfo info = new(); + try + { + info = HPatch.CreateInstance(pos => CreatePatchStream(patchFilePath, pos)); + PrintFileInfo(info); + if (isVerbosePrint && + HPatch.TryGetDirectoryPatchMetadata(ref info, out DirectoryPatchMetadata directoryPatch)) + { + unsafe + { + if (directoryPatch.InputPathListP != null) + { + PrintPathList("Input Path List:", + directoryPatch.InputPathListP, + directoryPatch.InputFileSizeListP, + directoryPatch.InputFileIndexListP, + null); + } + + if (directoryPatch.OutputPathListP != null) + { + PrintPathList("Output Path List:", + directoryPatch.OutputPathListP, + directoryPatch.OutputFileSizeListP, + directoryPatch.OutputFileIndexListP, + directoryPatch.OutputFileHashesListP); + } + + if (directoryPatch.SameFilePathIndexPairP != null && + directoryPatch.SameFilePathCountSizeInfoP != null && + directoryPatch.InputPathListP != null && + directoryPatch.OutputPathListP != null) + { + Console.WriteLine("Same File Copy List:"); + + void* p = directoryPatch.SameFilePathIndexPairP; + int count = directoryPatch.SameFilePathCountSizeInfoP->Count; + var sameFileIndexPairSpan = new Span(p, count); + + Span inputPathList = HPatch.TryGetUnmanagedArraySpan(directoryPatch.InputPathListP); + Span outputPathList = HPatch.TryGetUnmanagedArraySpan(directoryPatch.OutputPathListP); + for (int i = 0; i < count; i++) + { + ref FileIndexPair pair = ref sameFileIndexPairSpan[i]; + ref Utf16UnmanagedString inputPath = ref inputPathList[pair.OldIndex]; + ref Utf16UnmanagedString outputPath = ref outputPathList[pair.NewIndex]; + + Console.WriteLine($" {inputPath} ->> {outputPath}"); + } + + Console.WriteLine(); + } + } + } + + static unsafe void PrintPathList( + string msgType, + UnmanagedArray* pathList, + UnmanagedArray* pathSizeList, + UnmanagedArray* pathFileIndexList, + UnmanagedArray* pathFileHashList) + { + Console.WriteLine(msgType); + Span pathArray = HPatch.TryGetUnmanagedArraySpan(pathList); + Span fileSizeArray = HPatch.TryGetUnmanagedArraySpan(pathSizeList); + Span fileHashArray = HPatch.TryGetUnmanagedArraySpan(pathFileHashList); + + for (int i = 0; i < pathArray.Length; i++) + { + ref Utf16UnmanagedString pathStr = ref pathArray[i]; + string pathType = GetPathType(ref pathStr, out bool isDirectory); + + if (isDirectory) + { + Console.WriteLine($" {pathType}: {(pathStr == "" ? "(root)" : pathStr)}"); + } + } + + Span fileIndexList = HPatch.TryGetUnmanagedArraySpan(pathFileIndexList); + for (int i = 0; i < fileIndexList.Length; i++) + { + ref int index = ref fileIndexList[i]; + ref Utf16UnmanagedString pathStr = ref pathArray[index]; + + string tail = ""; + if (!fileSizeArray.IsEmpty) + { + ref long pathSize = ref fileSizeArray[i]; + tail = $" ({pathSize} bytes)"; + } + +#if NET6_0_OR_GREATER + if (!fileHashArray.IsEmpty) + { + ref long fileHash = ref fileHashArray[i]; + Span hashBytes = MemoryMarshal.AsBytes(new Span(Unsafe.AsPointer(ref fileHash), sizeof(long))); + + string hexString = Convert.ToHexString(hashBytes); + tail += $" (Hash: {hexString})"; + } +#endif + + Console.WriteLine($" File: {(pathStr == "" ? "(root)" : pathStr)}{tail}"); + } + + Console.WriteLine(); + } + + static string GetPathType(ref Utf16UnmanagedString inputPathStr, out bool isDirectory) + { + ReadOnlySpan inputPathSpan = inputPathStr; + if (inputPathSpan.IsEmpty) + { + isDirectory = true; + return "Directory"; + } + + // ReSharper disable once AssignmentInConditionalExpression + return (isDirectory = !inputPathSpan.IsEmpty && inputPathSpan[^1] is '/' or '\\') + ? "Directory" + : "File"; + } + } + catch (Exception ex) + { + Console.WriteLine( + $"An error has occurred! [{ex.GetType().Name}]: {ex.Message}\r\nStack Trace:\r\n{ex.StackTrace}"); + context.ExitCode = int.MinValue; +#if DEBUG + throw; +#endif + } + finally + { + info.Dispose(); + } + }); + } + + public static int Main(params string[] args) + { + AddCommandPatch(Command); + AddCommandInfo(Command); + return Command.Invoke(args); + } + +#if NET6_0_OR_GREATER + private static string DetermineSIMDCapability(PatchOptions options) + { + return options.UseSIMD switch + { + true when Avx2.IsSupported => "true (AVX2)", + true when Sse2.IsSupported => "true (SSE2)", + true when Vector.IsHardwareAccelerated => "true {Runtime-determined}", + _ => "false (Scalar)" + }; + } +#endif + + private static unsafe void PrintFileInfo(HDiffInfo info) + { + StringBuilder sb = new(); + if (HPatch.TryGetPatchMetadata(ref info, out PatchMetadata patchMetadata)) + { + sb.AppendLine($""" + Generic Patch Info: + Diff Type : {info.MagicType} + Compression Type : {info.CompressionType} + Input Size : {patchMetadata.DiffOldSize} + Output Size : {patchMetadata.DiffNewSize} + RLE Cover Info Count : {patchMetadata.CoverDataCount} + RLE Cover Info Size : {patchMetadata.CoverDataSizeP->Size} (Compressed Size: {patchMetadata.CoverDataSizeP->CompressedSize}) + RLE Control Size : {patchMetadata.RleControlDataSizeP->Size} (Compressed Size: {patchMetadata.RleControlDataSizeP->CompressedSize}) + RLE Code Size : {patchMetadata.RleCodeDataSizeP->Size} (Compressed Size: {patchMetadata.RleCodeDataSizeP->CompressedSize}) + RLE New Data Size : {patchMetadata.NewDiffDataSizeP->Size} (Compressed Size: {patchMetadata.NewDiffDataSizeP->CompressedSize}) + """); + } + + if (HPatch.TryGetDirectoryPatchMetadata(ref info, out DirectoryPatchMetadata dirPatchMetadata)) + { + sb.AppendLine($""" + + Directory Patch Info (HDiff19 Extension): + Checksum Type : {info.ChecksumType} + New Path Count : {dirPatchMetadata.OutputPathListP->Length} (File Count: {dirPatchMetadata.SameFilePathCountSizeInfoP->Count + dirPatchMetadata.OutputFileIndexListP->Length}) + Identical File Count : {dirPatchMetadata.SameFilePathCountSizeInfoP->Count} (Total Size: {dirPatchMetadata.SameFilePathCountSizeInfoP->Size}) + Input Reference File Count : {dirPatchMetadata.InputFileIndexListP->Length} (Total Size: {dirPatchMetadata.InputPathCountSizeInfoP->Size}) + Output Reference File Count : {dirPatchMetadata.OutputFileIndexListP->Length} (Total Size: {dirPatchMetadata.OutputPathCountSizeInfoP->Size}) + Input Total File Size : {dirPatchMetadata.InputPathCountSizeInfoP->Size + dirPatchMetadata.SameFilePathCountSizeInfoP->Size} + Output Total File Size : {dirPatchMetadata.OutputPathCountSizeInfoP->Size + dirPatchMetadata.SameFilePathCountSizeInfoP->Size} + """); + + if (dirPatchMetadata.InputFileSizeListP != null && + dirPatchMetadata.OutputFileHashesListP != null) + { + sb.AppendLine($""" + + Kuro Games HDiff Extension Info: + Input File Asserted Count : {dirPatchMetadata.InputFileSizeListP->Length} + Output File Hashes Count : {dirPatchMetadata.OutputFileHashesListP->Length} + """); + } + } + + Console.WriteLine(sb.ToString()); + } + + private static void PrintFileInfo(HDiffInfo info, + string inputPath, + string patchPath, + string outputPath, + PatchOptions options, + PatchPreset patchPresetType) + { + StringBuilder sb = new(); + sb.AppendLine($""" + Input Path : {inputPath} + Output Path : {outputPath} + Patch Path : {patchPath} + + Options: + Preset : {patchPresetType} + Max. Parallel Threads : {options.ParallelThreads} + Copy Buffer Size : {options.CopyBufferSize} + RLE Reader Buffer Size : {options.ReaderBufferSize} + Patch Worker Buffer Size : {options.PatchWorkerBufferSize} + """); + +#if NET6_0_OR_GREATER + sb.AppendLine($" Use SIMD? : {DetermineSIMDCapability(options)}"); +#endif + Console.WriteLine(sb.ToString()); + + PrintFileInfo(info); + } + + private static double lastSpeed; + private static TimeSpan lastRemainedTime = TimeSpan.Zero; + + public static void PatchProgress(long totalProcessed, long totalSize, int written) + { + double speed = CalculateSpeed(written); + double percent = Math.Round(totalProcessed / (double)totalSize * 100, 2); + + if (CheckIfNeedRefreshStopwatch()) + { + lastRemainedTime = TimeSpan.FromSeconds((totalSize - totalProcessed) / UnZeroed(speed)); + Interlocked.Exchange(ref lastSpeed, speed); + } + + Console.Write( + $"Patching: {percent}% | " + + $"{SummarizeSizeSimple(totalProcessed)}/{SummarizeSizeSimple(totalSize)} " + + $"@{SummarizeSizeSimple(lastSpeed)}/s " + + $"[{string.Format("{0:hh}h:{0:mm}m:{0:ss}s remaining", lastRemainedTime)}] \r"); + + return; + static double UnZeroed(double input) => Math.Max(input, 1); + } + private const double ScOneSecond = 1000; + private static long _scLastTick = Environment.TickCount; + private static long _scLastReceivedBytes; + private static double _scLastSpeed; + private static int _riLastTick = Environment.TickCount; + + private static double CalculateSpeed(long receivedBytes) => CalculateSpeed(receivedBytes, ref _scLastSpeed, ref _scLastReceivedBytes, ref _scLastTick); + + private static double CalculateSpeed(long receivedBytes, ref double lastSpeedToUse, ref long lastReceivedBytesToUse, ref long lastTickToUse) + { + long currentTick = Environment.TickCount - lastTickToUse + 1; + long totalReceivedInSecond = Interlocked.Add(ref lastReceivedBytesToUse, receivedBytes); + double speed = totalReceivedInSecond * ScOneSecond / currentTick; + + if (!(currentTick > ScOneSecond)) + { + return lastSpeedToUse; + } + + lastSpeedToUse = speed; + _ = Interlocked.Exchange(ref lastSpeedToUse, speed); + _ = Interlocked.Exchange(ref lastReceivedBytesToUse, 0); + _ = Interlocked.Exchange(ref lastTickToUse, Environment.TickCount); + return lastSpeedToUse; + } + + public static bool CheckIfNeedRefreshStopwatch() + { + int currentTick = Environment.TickCount - _riLastTick; + if (currentTick <= RefreshInterval) + { + return false; + } + + Interlocked.Exchange(ref _riLastTick, Environment.TickCount); + return true; + } + + private static string SummarizeSizeSimple(double value, int decimalPlaces = 2) + { + byte mag = (byte)Math.Log(value, 1000); + + return $"{Math.Round(value / (1L << (mag * 10)), decimalPlaces)} {SizeSuffixes[mag]}"; + } +} diff --git a/SharpHPatchZ.Program/Properties/launchSettings.json b/SharpHPatchZ.Program/Properties/launchSettings.json new file mode 100644 index 0000000..10c5aee --- /dev/null +++ b/SharpHPatchZ.Program/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "SharpHDiffPatch-bin": { + "commandName": "Project", + "commandLineArgs": "G:\\Hi3SEA G:\\Hi3SEAtoCNExecOnly.lzma2.diff G:\\Hi3CN-dotnet" + } + } +} \ No newline at end of file diff --git a/SharpHPatchZ.Program/SharpHPatchZ.Program.csproj b/SharpHPatchZ.Program/SharpHPatchZ.Program.csproj new file mode 100644 index 0000000..ab95c1e --- /dev/null +++ b/SharpHPatchZ.Program/SharpHPatchZ.Program.csproj @@ -0,0 +1,57 @@ + + + + Exe + net461;net11.0 + SharpHPatchZ.Program + SharpHPatchZ.Program + disable + enable + x64;ARM64;AnyCPU;x86 + 3.0.0 + true + Debug;Release + 14 + + + + + + + + + + $(DefineConstants);AOT + + + true + Speed + Speed + true + true + false + false + true + true + true + true + true + + + false + false + true + true + false + false + + + false + false + false + false + false + false + false + + \ No newline at end of file diff --git a/SharpHDiffPatch/System.CommandLine/Argument.cs b/SharpHPatchZ.Program/System.CommandLine/Argument.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Argument.cs rename to SharpHPatchZ.Program/System.CommandLine/Argument.cs diff --git a/SharpHDiffPatch/System.CommandLine/ArgumentArity.cs b/SharpHPatchZ.Program/System.CommandLine/ArgumentArity.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/ArgumentArity.cs rename to SharpHPatchZ.Program/System.CommandLine/ArgumentArity.cs diff --git a/SharpHDiffPatch/System.CommandLine/ArgumentExtensions.cs b/SharpHPatchZ.Program/System.CommandLine/ArgumentExtensions.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/ArgumentExtensions.cs rename to SharpHPatchZ.Program/System.CommandLine/ArgumentExtensions.cs diff --git a/SharpHDiffPatch/System.CommandLine/Argument{T}.cs b/SharpHPatchZ.Program/System.CommandLine/Argument{T}.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Argument{T}.cs rename to SharpHPatchZ.Program/System.CommandLine/Argument{T}.cs diff --git a/SharpHDiffPatch/System.CommandLine/Binding/ArgumentConversionResult.cs b/SharpHPatchZ.Program/System.CommandLine/Binding/ArgumentConversionResult.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Binding/ArgumentConversionResult.cs rename to SharpHPatchZ.Program/System.CommandLine/Binding/ArgumentConversionResult.cs diff --git a/SharpHDiffPatch/System.CommandLine/Binding/ArgumentConversionResultType.cs b/SharpHPatchZ.Program/System.CommandLine/Binding/ArgumentConversionResultType.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Binding/ArgumentConversionResultType.cs rename to SharpHPatchZ.Program/System.CommandLine/Binding/ArgumentConversionResultType.cs diff --git a/SharpHDiffPatch/System.CommandLine/Binding/ArgumentConverter.DefaultValues.cs b/SharpHPatchZ.Program/System.CommandLine/Binding/ArgumentConverter.DefaultValues.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Binding/ArgumentConverter.DefaultValues.cs rename to SharpHPatchZ.Program/System.CommandLine/Binding/ArgumentConverter.DefaultValues.cs diff --git a/SharpHDiffPatch/System.CommandLine/Binding/ArgumentConverter.StringConverters.cs b/SharpHPatchZ.Program/System.CommandLine/Binding/ArgumentConverter.StringConverters.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Binding/ArgumentConverter.StringConverters.cs rename to SharpHPatchZ.Program/System.CommandLine/Binding/ArgumentConverter.StringConverters.cs diff --git a/SharpHDiffPatch/System.CommandLine/Binding/ArgumentConverter.cs b/SharpHPatchZ.Program/System.CommandLine/Binding/ArgumentConverter.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Binding/ArgumentConverter.cs rename to SharpHPatchZ.Program/System.CommandLine/Binding/ArgumentConverter.cs diff --git a/SharpHDiffPatch/System.CommandLine/Binding/BinderBase{T}.cs b/SharpHPatchZ.Program/System.CommandLine/Binding/BinderBase{T}.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Binding/BinderBase{T}.cs rename to SharpHPatchZ.Program/System.CommandLine/Binding/BinderBase{T}.cs diff --git a/SharpHDiffPatch/System.CommandLine/Binding/BindingContext.cs b/SharpHPatchZ.Program/System.CommandLine/Binding/BindingContext.cs similarity index 97% rename from SharpHDiffPatch/System.CommandLine/Binding/BindingContext.cs rename to SharpHPatchZ.Program/System.CommandLine/Binding/BindingContext.cs index 7f2cbb5..bdb33a4 100644 --- a/SharpHDiffPatch/System.CommandLine/Binding/BindingContext.cs +++ b/SharpHPatchZ.Program/System.CommandLine/Binding/BindingContext.cs @@ -72,7 +72,10 @@ public void AddService(Func factory) internal bool TryGetValueSource( IValueDescriptor valueDescriptor, - [MaybeNullWhen(false)] out IValueSource valueSource) +#if NET6_0_OR_GREATER + [MaybeNullWhen(false)] +#endif + out IValueSource valueSource) { if (ServiceProvider.AvailableServiceTypes.Contains(valueDescriptor.ValueType)) { diff --git a/SharpHDiffPatch/System.CommandLine/Binding/BoundValue.cs b/SharpHPatchZ.Program/System.CommandLine/Binding/BoundValue.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Binding/BoundValue.cs rename to SharpHPatchZ.Program/System.CommandLine/Binding/BoundValue.cs diff --git a/SharpHDiffPatch/System.CommandLine/Binding/IValueDescriptor.cs b/SharpHPatchZ.Program/System.CommandLine/Binding/IValueDescriptor.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Binding/IValueDescriptor.cs rename to SharpHPatchZ.Program/System.CommandLine/Binding/IValueDescriptor.cs diff --git a/SharpHDiffPatch/System.CommandLine/Binding/IValueDescriptor{T}.cs b/SharpHPatchZ.Program/System.CommandLine/Binding/IValueDescriptor{T}.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Binding/IValueDescriptor{T}.cs rename to SharpHPatchZ.Program/System.CommandLine/Binding/IValueDescriptor{T}.cs diff --git a/SharpHDiffPatch/System.CommandLine/Binding/IValueSource.cs b/SharpHPatchZ.Program/System.CommandLine/Binding/IValueSource.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Binding/IValueSource.cs rename to SharpHPatchZ.Program/System.CommandLine/Binding/IValueSource.cs diff --git a/SharpHDiffPatch/System.CommandLine/Binding/ServiceProviderExtensions.cs b/SharpHPatchZ.Program/System.CommandLine/Binding/ServiceProviderExtensions.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Binding/ServiceProviderExtensions.cs rename to SharpHPatchZ.Program/System.CommandLine/Binding/ServiceProviderExtensions.cs diff --git a/SharpHDiffPatch/System.CommandLine/Binding/ServiceProviderValueSource.cs b/SharpHPatchZ.Program/System.CommandLine/Binding/ServiceProviderValueSource.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Binding/ServiceProviderValueSource.cs rename to SharpHPatchZ.Program/System.CommandLine/Binding/ServiceProviderValueSource.cs diff --git a/SharpHDiffPatch/System.CommandLine/Binding/TryConvertArgument.cs b/SharpHPatchZ.Program/System.CommandLine/Binding/TryConvertArgument.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Binding/TryConvertArgument.cs rename to SharpHPatchZ.Program/System.CommandLine/Binding/TryConvertArgument.cs diff --git a/SharpHDiffPatch/System.CommandLine/Binding/TypeExtensions.cs b/SharpHPatchZ.Program/System.CommandLine/Binding/TypeExtensions.cs similarity index 94% rename from SharpHDiffPatch/System.CommandLine/Binding/TypeExtensions.cs rename to SharpHPatchZ.Program/System.CommandLine/Binding/TypeExtensions.cs index b0b680a..79f0156 100644 --- a/SharpHDiffPatch/System.CommandLine/Binding/TypeExtensions.cs +++ b/SharpHPatchZ.Program/System.CommandLine/Binding/TypeExtensions.cs @@ -51,7 +51,10 @@ internal static bool IsEnumerable(this Type type) internal static bool TryGetNullableType( this Type type, - [NotNullWhen(true)] out Type? nullableType) +#if NET6_0_OR_GREATER + [NotNullWhen(true)] +#endif + out Type? nullableType) { nullableType = Nullable.GetUnderlyingType(type); return nullableType is not null; diff --git a/SharpHDiffPatch/System.CommandLine/Binding/ValueDescriptorDefaultValueSource.cs b/SharpHPatchZ.Program/System.CommandLine/Binding/ValueDescriptorDefaultValueSource.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Binding/ValueDescriptorDefaultValueSource.cs rename to SharpHPatchZ.Program/System.CommandLine/Binding/ValueDescriptorDefaultValueSource.cs diff --git a/SharpHDiffPatch/System.CommandLine/Builder/CommandLineBuilder.cs b/SharpHPatchZ.Program/System.CommandLine/Builder/CommandLineBuilder.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Builder/CommandLineBuilder.cs rename to SharpHPatchZ.Program/System.CommandLine/Builder/CommandLineBuilder.cs diff --git a/SharpHDiffPatch/System.CommandLine/Builder/CommandLineBuilderExtensions.cs b/SharpHPatchZ.Program/System.CommandLine/Builder/CommandLineBuilderExtensions.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Builder/CommandLineBuilderExtensions.cs rename to SharpHPatchZ.Program/System.CommandLine/Builder/CommandLineBuilderExtensions.cs diff --git a/SharpHDiffPatch/System.CommandLine/Command.cs b/SharpHPatchZ.Program/System.CommandLine/Command.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Command.cs rename to SharpHPatchZ.Program/System.CommandLine/Command.cs diff --git a/SharpHDiffPatch/System.CommandLine/CommandExtensions.cs b/SharpHPatchZ.Program/System.CommandLine/CommandExtensions.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/CommandExtensions.cs rename to SharpHPatchZ.Program/System.CommandLine/CommandExtensions.cs diff --git a/SharpHDiffPatch/System.CommandLine/CommandLineConfiguration.cs b/SharpHPatchZ.Program/System.CommandLine/CommandLineConfiguration.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/CommandLineConfiguration.cs rename to SharpHPatchZ.Program/System.CommandLine/CommandLineConfiguration.cs diff --git a/SharpHDiffPatch/System.CommandLine/CommandLineConfigurationException.cs b/SharpHPatchZ.Program/System.CommandLine/CommandLineConfigurationException.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/CommandLineConfigurationException.cs rename to SharpHPatchZ.Program/System.CommandLine/CommandLineConfigurationException.cs diff --git a/SharpHDiffPatch/System.CommandLine/CompletionSourceExtensions.cs b/SharpHPatchZ.Program/System.CommandLine/CompletionSourceExtensions.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/CompletionSourceExtensions.cs rename to SharpHPatchZ.Program/System.CommandLine/CompletionSourceExtensions.cs diff --git a/SharpHDiffPatch/System.CommandLine/CompletionSourceList.cs b/SharpHPatchZ.Program/System.CommandLine/CompletionSourceList.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/CompletionSourceList.cs rename to SharpHPatchZ.Program/System.CommandLine/CompletionSourceList.cs diff --git a/SharpHDiffPatch/System.CommandLine/Completions/AnonymousCompletionSource.cs b/SharpHPatchZ.Program/System.CommandLine/Completions/AnonymousCompletionSource.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Completions/AnonymousCompletionSource.cs rename to SharpHPatchZ.Program/System.CommandLine/Completions/AnonymousCompletionSource.cs diff --git a/SharpHDiffPatch/System.CommandLine/Completions/CompletionContext.cs b/SharpHPatchZ.Program/System.CommandLine/Completions/CompletionContext.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Completions/CompletionContext.cs rename to SharpHPatchZ.Program/System.CommandLine/Completions/CompletionContext.cs diff --git a/SharpHDiffPatch/System.CommandLine/Completions/CompletionDelegate.cs b/SharpHPatchZ.Program/System.CommandLine/Completions/CompletionDelegate.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Completions/CompletionDelegate.cs rename to SharpHPatchZ.Program/System.CommandLine/Completions/CompletionDelegate.cs diff --git a/SharpHDiffPatch/System.CommandLine/Completions/CompletionItem.cs b/SharpHPatchZ.Program/System.CommandLine/Completions/CompletionItem.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Completions/CompletionItem.cs rename to SharpHPatchZ.Program/System.CommandLine/Completions/CompletionItem.cs diff --git a/SharpHDiffPatch/System.CommandLine/Completions/CompletionItemKind.cs b/SharpHPatchZ.Program/System.CommandLine/Completions/CompletionItemKind.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Completions/CompletionItemKind.cs rename to SharpHPatchZ.Program/System.CommandLine/Completions/CompletionItemKind.cs diff --git a/SharpHDiffPatch/System.CommandLine/Completions/CompletionSource.cs b/SharpHPatchZ.Program/System.CommandLine/Completions/CompletionSource.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Completions/CompletionSource.cs rename to SharpHPatchZ.Program/System.CommandLine/Completions/CompletionSource.cs diff --git a/SharpHDiffPatch/System.CommandLine/Completions/ICompletionSource.cs b/SharpHPatchZ.Program/System.CommandLine/Completions/ICompletionSource.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Completions/ICompletionSource.cs rename to SharpHPatchZ.Program/System.CommandLine/Completions/ICompletionSource.cs diff --git a/SharpHDiffPatch/System.CommandLine/Completions/TextCompletionContext.cs b/SharpHPatchZ.Program/System.CommandLine/Completions/TextCompletionContext.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Completions/TextCompletionContext.cs rename to SharpHPatchZ.Program/System.CommandLine/Completions/TextCompletionContext.cs diff --git a/SharpHDiffPatch/System.CommandLine/Completions/TokenCompletionContext.cs b/SharpHPatchZ.Program/System.CommandLine/Completions/TokenCompletionContext.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Completions/TokenCompletionContext.cs rename to SharpHPatchZ.Program/System.CommandLine/Completions/TokenCompletionContext.cs diff --git a/SharpHDiffPatch/System.CommandLine/ConsoleExtensions.cs b/SharpHPatchZ.Program/System.CommandLine/ConsoleExtensions.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/ConsoleExtensions.cs rename to SharpHPatchZ.Program/System.CommandLine/ConsoleExtensions.cs diff --git a/SharpHDiffPatch/System.CommandLine/DebugAssert.cs b/SharpHPatchZ.Program/System.CommandLine/DebugAssert.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/DebugAssert.cs rename to SharpHPatchZ.Program/System.CommandLine/DebugAssert.cs diff --git a/SharpHDiffPatch/System.CommandLine/DictionaryExtensions.cs b/SharpHPatchZ.Program/System.CommandLine/DictionaryExtensions.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/DictionaryExtensions.cs rename to SharpHPatchZ.Program/System.CommandLine/DictionaryExtensions.cs diff --git a/SharpHDiffPatch/System.CommandLine/DirectiveCollection.cs b/SharpHPatchZ.Program/System.CommandLine/DirectiveCollection.cs similarity index 94% rename from SharpHDiffPatch/System.CommandLine/DirectiveCollection.cs rename to SharpHPatchZ.Program/System.CommandLine/DirectiveCollection.cs index 45a469d..636d8d8 100644 --- a/SharpHDiffPatch/System.CommandLine/DirectiveCollection.cs +++ b/SharpHPatchZ.Program/System.CommandLine/DirectiveCollection.cs @@ -52,7 +52,11 @@ public bool Contains(string name) /// The name of the directive. /// The values provided for the specified directive. /// if a directive with the specified name was parsed; otherwise, . - public bool TryGetValues(string name, [NotNullWhen(true)] out IReadOnlyList? values) + public bool TryGetValues(string name, +#if NET6_0_OR_GREATER + [NotNullWhen(true)] +#endif + out IReadOnlyList? values) { if (_directives is not null && _directives.TryGetValue(name, out var v)) diff --git a/SharpHDiffPatch/System.CommandLine/EnumerableExtensions.cs b/SharpHPatchZ.Program/System.CommandLine/EnumerableExtensions.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/EnumerableExtensions.cs rename to SharpHPatchZ.Program/System.CommandLine/EnumerableExtensions.cs diff --git a/SharpHDiffPatch/System.CommandLine/Handler.Action.cs b/SharpHPatchZ.Program/System.CommandLine/Handler.Action.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Handler.Action.cs rename to SharpHPatchZ.Program/System.CommandLine/Handler.Action.cs diff --git a/SharpHDiffPatch/System.CommandLine/Handler.Func.cs b/SharpHPatchZ.Program/System.CommandLine/Handler.Func.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Handler.Func.cs rename to SharpHPatchZ.Program/System.CommandLine/Handler.Func.cs diff --git a/SharpHDiffPatch/System.CommandLine/Handler.cs b/SharpHPatchZ.Program/System.CommandLine/Handler.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Handler.cs rename to SharpHPatchZ.Program/System.CommandLine/Handler.cs diff --git a/SharpHDiffPatch/System.CommandLine/Help/HelpBuilder.Default.cs b/SharpHPatchZ.Program/System.CommandLine/Help/HelpBuilder.Default.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Help/HelpBuilder.Default.cs rename to SharpHPatchZ.Program/System.CommandLine/Help/HelpBuilder.Default.cs diff --git a/SharpHDiffPatch/System.CommandLine/Help/HelpBuilder.cs b/SharpHPatchZ.Program/System.CommandLine/Help/HelpBuilder.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Help/HelpBuilder.cs rename to SharpHPatchZ.Program/System.CommandLine/Help/HelpBuilder.cs diff --git a/SharpHDiffPatch/System.CommandLine/Help/HelpBuilderExtensions.cs b/SharpHPatchZ.Program/System.CommandLine/Help/HelpBuilderExtensions.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Help/HelpBuilderExtensions.cs rename to SharpHPatchZ.Program/System.CommandLine/Help/HelpBuilderExtensions.cs diff --git a/SharpHDiffPatch/System.CommandLine/Help/HelpContext.cs b/SharpHPatchZ.Program/System.CommandLine/Help/HelpContext.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Help/HelpContext.cs rename to SharpHPatchZ.Program/System.CommandLine/Help/HelpContext.cs diff --git a/SharpHDiffPatch/System.CommandLine/Help/HelpOption.cs b/SharpHPatchZ.Program/System.CommandLine/Help/HelpOption.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Help/HelpOption.cs rename to SharpHPatchZ.Program/System.CommandLine/Help/HelpOption.cs diff --git a/SharpHDiffPatch/System.CommandLine/Help/HelpResult.cs b/SharpHPatchZ.Program/System.CommandLine/Help/HelpResult.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Help/HelpResult.cs rename to SharpHPatchZ.Program/System.CommandLine/Help/HelpResult.cs diff --git a/SharpHDiffPatch/System.CommandLine/Help/HelpSectionDelegate.cs b/SharpHPatchZ.Program/System.CommandLine/Help/HelpSectionDelegate.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Help/HelpSectionDelegate.cs rename to SharpHPatchZ.Program/System.CommandLine/Help/HelpSectionDelegate.cs diff --git a/SharpHDiffPatch/System.CommandLine/Help/TwoColumnHelpRow.cs b/SharpHPatchZ.Program/System.CommandLine/Help/TwoColumnHelpRow.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Help/TwoColumnHelpRow.cs rename to SharpHPatchZ.Program/System.CommandLine/Help/TwoColumnHelpRow.cs diff --git a/SharpHDiffPatch/System.CommandLine/Help/VersionOption.cs b/SharpHPatchZ.Program/System.CommandLine/Help/VersionOption.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Help/VersionOption.cs rename to SharpHPatchZ.Program/System.CommandLine/Help/VersionOption.cs diff --git a/SharpHDiffPatch/System.CommandLine/IConsole.cs b/SharpHPatchZ.Program/System.CommandLine/IConsole.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/IConsole.cs rename to SharpHPatchZ.Program/System.CommandLine/IConsole.cs diff --git a/SharpHDiffPatch/System.CommandLine/IO/ConsoleExtensions.cs b/SharpHPatchZ.Program/System.CommandLine/IO/ConsoleExtensions.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/IO/ConsoleExtensions.cs rename to SharpHPatchZ.Program/System.CommandLine/IO/ConsoleExtensions.cs diff --git a/SharpHDiffPatch/System.CommandLine/IO/IStandardError.cs b/SharpHPatchZ.Program/System.CommandLine/IO/IStandardError.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/IO/IStandardError.cs rename to SharpHPatchZ.Program/System.CommandLine/IO/IStandardError.cs diff --git a/SharpHDiffPatch/System.CommandLine/IO/IStandardIn.cs b/SharpHPatchZ.Program/System.CommandLine/IO/IStandardIn.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/IO/IStandardIn.cs rename to SharpHPatchZ.Program/System.CommandLine/IO/IStandardIn.cs diff --git a/SharpHDiffPatch/System.CommandLine/IO/IStandardOut.cs b/SharpHPatchZ.Program/System.CommandLine/IO/IStandardOut.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/IO/IStandardOut.cs rename to SharpHPatchZ.Program/System.CommandLine/IO/IStandardOut.cs diff --git a/SharpHDiffPatch/System.CommandLine/IO/IStandardStreamWriter.cs b/SharpHPatchZ.Program/System.CommandLine/IO/IStandardStreamWriter.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/IO/IStandardStreamWriter.cs rename to SharpHPatchZ.Program/System.CommandLine/IO/IStandardStreamWriter.cs diff --git a/SharpHDiffPatch/System.CommandLine/IO/StandardStreamWriter.cs b/SharpHPatchZ.Program/System.CommandLine/IO/StandardStreamWriter.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/IO/StandardStreamWriter.cs rename to SharpHPatchZ.Program/System.CommandLine/IO/StandardStreamWriter.cs diff --git a/SharpHDiffPatch/System.CommandLine/IO/SystemConsole.cs b/SharpHPatchZ.Program/System.CommandLine/IO/SystemConsole.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/IO/SystemConsole.cs rename to SharpHPatchZ.Program/System.CommandLine/IO/SystemConsole.cs diff --git a/SharpHDiffPatch/System.CommandLine/IO/TestConsole.cs b/SharpHPatchZ.Program/System.CommandLine/IO/TestConsole.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/IO/TestConsole.cs rename to SharpHPatchZ.Program/System.CommandLine/IO/TestConsole.cs diff --git a/SharpHDiffPatch/System.CommandLine/IdentifierSymbol.cs b/SharpHPatchZ.Program/System.CommandLine/IdentifierSymbol.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/IdentifierSymbol.cs rename to SharpHPatchZ.Program/System.CommandLine/IdentifierSymbol.cs diff --git a/SharpHDiffPatch/System.CommandLine/Invocation/AnonymousCommandHandler.cs b/SharpHPatchZ.Program/System.CommandLine/Invocation/AnonymousCommandHandler.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Invocation/AnonymousCommandHandler.cs rename to SharpHPatchZ.Program/System.CommandLine/Invocation/AnonymousCommandHandler.cs diff --git a/SharpHDiffPatch/System.CommandLine/Invocation/FeatureRegistration.cs b/SharpHPatchZ.Program/System.CommandLine/Invocation/FeatureRegistration.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Invocation/FeatureRegistration.cs rename to SharpHPatchZ.Program/System.CommandLine/Invocation/FeatureRegistration.cs diff --git a/SharpHDiffPatch/System.CommandLine/Invocation/ICommandHandler.cs b/SharpHPatchZ.Program/System.CommandLine/Invocation/ICommandHandler.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Invocation/ICommandHandler.cs rename to SharpHPatchZ.Program/System.CommandLine/Invocation/ICommandHandler.cs diff --git a/SharpHDiffPatch/System.CommandLine/Invocation/IInvocationResult.cs b/SharpHPatchZ.Program/System.CommandLine/Invocation/IInvocationResult.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Invocation/IInvocationResult.cs rename to SharpHPatchZ.Program/System.CommandLine/Invocation/IInvocationResult.cs diff --git a/SharpHDiffPatch/System.CommandLine/Invocation/InvocationContext.cs b/SharpHPatchZ.Program/System.CommandLine/Invocation/InvocationContext.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Invocation/InvocationContext.cs rename to SharpHPatchZ.Program/System.CommandLine/Invocation/InvocationContext.cs diff --git a/SharpHDiffPatch/System.CommandLine/Invocation/InvocationMiddleware.cs b/SharpHPatchZ.Program/System.CommandLine/Invocation/InvocationMiddleware.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Invocation/InvocationMiddleware.cs rename to SharpHPatchZ.Program/System.CommandLine/Invocation/InvocationMiddleware.cs diff --git a/SharpHDiffPatch/System.CommandLine/Invocation/InvocationPipeline.cs b/SharpHPatchZ.Program/System.CommandLine/Invocation/InvocationPipeline.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Invocation/InvocationPipeline.cs rename to SharpHPatchZ.Program/System.CommandLine/Invocation/InvocationPipeline.cs diff --git a/SharpHDiffPatch/System.CommandLine/Invocation/MiddlewareOrder.cs b/SharpHPatchZ.Program/System.CommandLine/Invocation/MiddlewareOrder.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Invocation/MiddlewareOrder.cs rename to SharpHPatchZ.Program/System.CommandLine/Invocation/MiddlewareOrder.cs diff --git a/SharpHDiffPatch/System.CommandLine/Invocation/ParseDirectiveResult.cs b/SharpHPatchZ.Program/System.CommandLine/Invocation/ParseDirectiveResult.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Invocation/ParseDirectiveResult.cs rename to SharpHPatchZ.Program/System.CommandLine/Invocation/ParseDirectiveResult.cs diff --git a/SharpHDiffPatch/System.CommandLine/Invocation/ParseErrorResult.cs b/SharpHPatchZ.Program/System.CommandLine/Invocation/ParseErrorResult.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Invocation/ParseErrorResult.cs rename to SharpHPatchZ.Program/System.CommandLine/Invocation/ParseErrorResult.cs diff --git a/SharpHDiffPatch/System.CommandLine/Invocation/Process.cs b/SharpHPatchZ.Program/System.CommandLine/Invocation/Process.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Invocation/Process.cs rename to SharpHPatchZ.Program/System.CommandLine/Invocation/Process.cs diff --git a/SharpHDiffPatch/System.CommandLine/Invocation/ServiceProvider.cs b/SharpHPatchZ.Program/System.CommandLine/Invocation/ServiceProvider.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Invocation/ServiceProvider.cs rename to SharpHPatchZ.Program/System.CommandLine/Invocation/ServiceProvider.cs diff --git a/SharpHDiffPatch/System.CommandLine/Invocation/SuggestDirectiveResult.cs b/SharpHPatchZ.Program/System.CommandLine/Invocation/SuggestDirectiveResult.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Invocation/SuggestDirectiveResult.cs rename to SharpHPatchZ.Program/System.CommandLine/Invocation/SuggestDirectiveResult.cs diff --git a/SharpHDiffPatch/System.CommandLine/Invocation/TypoCorrection.cs b/SharpHPatchZ.Program/System.CommandLine/Invocation/TypoCorrection.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Invocation/TypoCorrection.cs rename to SharpHPatchZ.Program/System.CommandLine/Invocation/TypoCorrection.cs diff --git a/SharpHDiffPatch/System.CommandLine/LocalizationResources.cs b/SharpHPatchZ.Program/System.CommandLine/LocalizationResources.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/LocalizationResources.cs rename to SharpHPatchZ.Program/System.CommandLine/LocalizationResources.cs diff --git a/SharpHDiffPatch/System.CommandLine/Option.cs b/SharpHPatchZ.Program/System.CommandLine/Option.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Option.cs rename to SharpHPatchZ.Program/System.CommandLine/Option.cs diff --git a/SharpHDiffPatch/System.CommandLine/OptionExtensions.cs b/SharpHPatchZ.Program/System.CommandLine/OptionExtensions.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/OptionExtensions.cs rename to SharpHPatchZ.Program/System.CommandLine/OptionExtensions.cs diff --git a/SharpHDiffPatch/System.CommandLine/Option{T}.cs b/SharpHPatchZ.Program/System.CommandLine/Option{T}.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Option{T}.cs rename to SharpHPatchZ.Program/System.CommandLine/Option{T}.cs diff --git a/SharpHDiffPatch/System.CommandLine/ParentNode.cs b/SharpHPatchZ.Program/System.CommandLine/ParentNode.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/ParentNode.cs rename to SharpHPatchZ.Program/System.CommandLine/ParentNode.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/ArgumentResult.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/ArgumentResult.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/ArgumentResult.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/ArgumentResult.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/CommandArgumentNode.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/CommandArgumentNode.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/CommandArgumentNode.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/CommandArgumentNode.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/CommandLineStringSplitter.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/CommandLineStringSplitter.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/CommandLineStringSplitter.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/CommandLineStringSplitter.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/CommandNode.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/CommandNode.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/CommandNode.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/CommandNode.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/CommandResult.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/CommandResult.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/CommandResult.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/CommandResult.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/DirectiveNode.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/DirectiveNode.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/DirectiveNode.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/DirectiveNode.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/NonterminalSyntaxNode.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/NonterminalSyntaxNode.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/NonterminalSyntaxNode.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/NonterminalSyntaxNode.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/OptionArgumentNode.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/OptionArgumentNode.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/OptionArgumentNode.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/OptionArgumentNode.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/OptionNode.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/OptionNode.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/OptionNode.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/OptionNode.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/OptionResult.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/OptionResult.cs similarity index 99% rename from SharpHDiffPatch/System.CommandLine/Parsing/OptionResult.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/OptionResult.cs index e1c2b95..38143d5 100644 --- a/SharpHDiffPatch/System.CommandLine/Parsing/OptionResult.cs +++ b/SharpHPatchZ.Program/System.CommandLine/Parsing/OptionResult.cs @@ -50,7 +50,9 @@ internal OptionResult( /// Gets the parsed value or the default value for . /// /// The parsed value or the default value for +#if NET6_0_OR_GREATER [return: MaybeNull] +#endif public T GetValueOrDefault() => this.ConvertIfNeeded(typeof(T)) .GetValueOrDefault(); diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/OptionResultExtensions.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/OptionResultExtensions.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/OptionResultExtensions.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/OptionResultExtensions.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/ParseArgument{T}.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/ParseArgument{T}.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/ParseArgument{T}.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/ParseArgument{T}.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/ParseError.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/ParseError.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/ParseError.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/ParseError.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/ParseOperation.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/ParseOperation.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/ParseOperation.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/ParseOperation.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/ParseResult.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/ParseResult.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/ParseResult.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/ParseResult.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/ParseResultExtensions.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/ParseResultExtensions.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/ParseResultExtensions.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/ParseResultExtensions.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/ParseResultVisitor.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/ParseResultVisitor.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/ParseResultVisitor.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/ParseResultVisitor.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/Parser.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/Parser.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/Parser.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/Parser.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/ParserExtensions.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/ParserExtensions.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/ParserExtensions.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/ParserExtensions.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/RootCommandResult.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/RootCommandResult.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/RootCommandResult.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/RootCommandResult.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/StringExtensions.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/StringExtensions.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/StringExtensions.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/StringExtensions.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/SymbolResult.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/SymbolResult.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/SymbolResult.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/SymbolResult.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/SymbolResultExtensions.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/SymbolResultExtensions.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/SymbolResultExtensions.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/SymbolResultExtensions.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/SymbolResultVisitor.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/SymbolResultVisitor.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/SymbolResultVisitor.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/SymbolResultVisitor.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/SyntaxNode.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/SyntaxNode.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/SyntaxNode.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/SyntaxNode.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/Token.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/Token.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/Token.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/Token.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/TokenType.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/TokenType.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/TokenType.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/TokenType.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/TokenizeResult.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/TokenizeResult.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/TokenizeResult.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/TokenizeResult.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/TryReplaceToken.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/TryReplaceToken.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/TryReplaceToken.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/TryReplaceToken.cs diff --git a/SharpHDiffPatch/System.CommandLine/Parsing/ValidateSymbolResult.cs b/SharpHPatchZ.Program/System.CommandLine/Parsing/ValidateSymbolResult.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Parsing/ValidateSymbolResult.cs rename to SharpHPatchZ.Program/System.CommandLine/Parsing/ValidateSymbolResult.cs diff --git a/SharpHDiffPatch/System.CommandLine/Platform.cs b/SharpHPatchZ.Program/System.CommandLine/Platform.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Platform.cs rename to SharpHPatchZ.Program/System.CommandLine/Platform.cs diff --git a/SharpHDiffPatch/System.CommandLine/Properties/Resources.Designer.cs b/SharpHPatchZ.Program/System.CommandLine/Properties/Resources.Designer.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Properties/Resources.Designer.cs rename to SharpHPatchZ.Program/System.CommandLine/Properties/Resources.Designer.cs diff --git a/SharpHDiffPatch/System.CommandLine/RootCommand.cs b/SharpHPatchZ.Program/System.CommandLine/RootCommand.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/RootCommand.cs rename to SharpHPatchZ.Program/System.CommandLine/RootCommand.cs diff --git a/SharpHDiffPatch/System.CommandLine/StringBuilderPool.cs b/SharpHPatchZ.Program/System.CommandLine/StringBuilderPool.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/StringBuilderPool.cs rename to SharpHPatchZ.Program/System.CommandLine/StringBuilderPool.cs diff --git a/SharpHDiffPatch/System.CommandLine/Symbol.cs b/SharpHPatchZ.Program/System.CommandLine/Symbol.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Symbol.cs rename to SharpHPatchZ.Program/System.CommandLine/Symbol.cs diff --git a/SharpHDiffPatch/System.CommandLine/SymbolExtensions.cs b/SharpHPatchZ.Program/System.CommandLine/SymbolExtensions.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/SymbolExtensions.cs rename to SharpHPatchZ.Program/System.CommandLine/SymbolExtensions.cs diff --git a/SharpHDiffPatch/System.CommandLine/System.Diagnostics.CodeAnalysis/DynamicallyAccessedMemberTypes.cs b/SharpHPatchZ.Program/System.CommandLine/System.Diagnostics.CodeAnalysis/DynamicallyAccessedMemberTypes.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/System.Diagnostics.CodeAnalysis/DynamicallyAccessedMemberTypes.cs rename to SharpHPatchZ.Program/System.CommandLine/System.Diagnostics.CodeAnalysis/DynamicallyAccessedMemberTypes.cs diff --git a/SharpHDiffPatch/System.CommandLine/System.Diagnostics.CodeAnalysis/DynamicallyAccessedMembersAttribute.cs b/SharpHPatchZ.Program/System.CommandLine/System.Diagnostics.CodeAnalysis/DynamicallyAccessedMembersAttribute.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/System.Diagnostics.CodeAnalysis/DynamicallyAccessedMembersAttribute.cs rename to SharpHPatchZ.Program/System.CommandLine/System.Diagnostics.CodeAnalysis/DynamicallyAccessedMembersAttribute.cs diff --git a/SharpHDiffPatch/System.CommandLine/System.Diagnostics.CodeAnalysis/UnconditionalSuppressMessageAttribute.cs b/SharpHPatchZ.Program/System.CommandLine/System.Diagnostics.CodeAnalysis/UnconditionalSuppressMessageAttribute.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/System.Diagnostics.CodeAnalysis/UnconditionalSuppressMessageAttribute.cs rename to SharpHPatchZ.Program/System.CommandLine/System.Diagnostics.CodeAnalysis/UnconditionalSuppressMessageAttribute.cs diff --git a/SharpHDiffPatch/System.CommandLine/System.Runtime.CompilerServices/IsExternalInit.cs b/SharpHPatchZ.Program/System.CommandLine/System.Runtime.CompilerServices/IsExternalInit.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/System.Runtime.CompilerServices/IsExternalInit.cs rename to SharpHPatchZ.Program/System.CommandLine/System.Runtime.CompilerServices/IsExternalInit.cs diff --git a/SharpHDiffPatch/System.CommandLine/Validate.cs b/SharpHPatchZ.Program/System.CommandLine/Validate.cs similarity index 100% rename from SharpHDiffPatch/System.CommandLine/Validate.cs rename to SharpHPatchZ.Program/System.CommandLine/Validate.cs diff --git a/SharpHPatchZ/Extension/BigArrayPool.cs b/SharpHPatchZ/Extension/BigArrayPool.cs new file mode 100644 index 0000000..203e6b3 --- /dev/null +++ b/SharpHPatchZ/Extension/BigArrayPool.cs @@ -0,0 +1,9 @@ +using System; +using System.Buffers; + +namespace SharpHPatchZ.Extension; + +internal static class BigArrayPool +{ + public static ArrayPool Shared { get; } = ArrayPool.Create(128 << 20, Environment.ProcessorCount << 10); +} diff --git a/SharpHPatchZ/Extension/ExceptionHelper.cs b/SharpHPatchZ/Extension/ExceptionHelper.cs new file mode 100644 index 0000000..707340c --- /dev/null +++ b/SharpHPatchZ/Extension/ExceptionHelper.cs @@ -0,0 +1,290 @@ +using System; +using System.IO; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using SharpHPatchZ.Header; +// ReSharper disable StringLiteralTypo +// ReSharper disable IdentifierTypo +// ReSharper disable InconsistentNaming + +namespace SharpHPatchZ.Extension; + +file static class Const +{ + internal static readonly Dictionary ExceptionToReturnCodeMap = new() + { + // Not Supported + { HDiffHeaderMagicNotSupported, 0x10 }, + { HDiffHeaderCompressionNotSupported, 0x11 }, + { HDiffHeaderChecksumNotSupported, 0x12 }, + { HDiffPatchFactoryNotSupported, 0x13 }, + + // Memory Allocation / Descriptor + { HDiffInfoNotAllocated, 0x30 }, + { HDiffInfoDirectoryPatchMetadataNotAllocated, 0x31 }, + { HDiffInfoPatchMetadataNotAllocated, 0x32 }, + { HDiffFILEDescriptorNull, 0x33 }, + { HDiffArgumentNull, 0x34 }, + + // IO / File / Paths + { HDiffHeaderSignatureEmptyOrUnreadable, 0x50 }, + { HDiffHeaderEndOfFileOrData, 0x51 }, + { HDiffPathIsEmptyOrInvalid, 0x52 }, + { HDiffIOException, 0x53 }, + { HDiffPatchInputPathNotExist, 0x54 }, + { HDiffPatchInputSizeMismatched, 0x55 }, + { HDiffPatchPathNotADirectory, 0x56 }, + { HDiffPatchPathNotAFile, 0x57 }, + { HDiffPatchInputFilesMismatched, 0x58 }, + { HDiffPatchKuroInputFileSizeMismatched, 0x59 }, + { HDiffStreamReadOutOfBound, 0x5A }, + { HDiffStringEncodingFailed, 0x5B }, + + // Decompression Initialization + { HDiffCompLZMAPropertyMissing, 0xA0 }, + { HDiffCompLZMA2DictionaryInvalid, 0xA1 }, + { HDiffCompLZMA2NoCompressedPayload, 0xA2 }, + { HDiffCompLZMADictionaryInvalidLength, 0xA3 }, + { HDiffCompLZMASizeTooSmallForDictionaryRead, 0xA4 } + }; + + public const string HDiffHeaderMagicNotSupported = "HDIFF_HeaderMagicNotSupported"; + public const string HDiffHeaderCompressionNotSupported = "HDIFF_HeaderCompressionNotSupported"; + public const string HDiffHeaderChecksumNotSupported = "HDIFF_HeaderChecksumNotSupported"; + public const string HDiffPatchFactoryNotSupported = "HDIFF_PatchFactoryNotSupported"; + + public const string HDiffInfoNotAllocated = "HDIFF_InfoNotAllocated"; + public const string HDiffInfoDirectoryPatchMetadataNotAllocated = "HDIFF_InfoDirectoryPatchMetadataNotAllocated"; + public const string HDiffInfoPatchMetadataNotAllocated = "HDIFF_InfoPatchMetadataNotAllocated"; + public const string HDiffFILEDescriptorNull = "HDIFF_FILEDescriptorNull"; + public const string HDiffArgumentNull = "HDIFF_ArgumentNull"; + + public const string HDiffHeaderSignatureEmptyOrUnreadable = "HDIFF_HeaderSignatureEmptyOrUnreadable"; + public const string HDiffHeaderEndOfFileOrData = "HDIFF_EndOfFileOrData"; + public const string HDiffPathIsEmptyOrInvalid = "HDIFF_PathIsEmptyOrInvalid"; + public const string HDiffIOException = "HDIFF_IOException"; + public const string HDiffPatchInputPathNotExist = "HDIFF_PatchInputPathNotExist"; + public const string HDiffPatchInputSizeMismatched = "HDIFF_PatchInputSizeMismatched"; + public const string HDiffPatchPathNotADirectory = "HDIFF_PatchPathNotADirectory"; + public const string HDiffPatchPathNotAFile = "HDIFF_PatchPathNotAFile"; + public const string HDiffPatchInputFilesMismatched = "HDIFF_PatchInputFilesMismatched"; + public const string HDiffPatchKuroInputFileSizeMismatched = "HDIFF_PatchKuroInputFileSizeMismatched"; + public const string HDiffStreamReadOutOfBound = "HDIFF_StreamReadOutOfBound"; + public const string HDiffStringEncodingFailed = "HDIFF_StringEncodingFailed"; + + public const string HDiffCompLZMAPropertyMissing = "HDIFF_CompLZMAPropertyMissing"; + public const string HDiffCompLZMA2DictionaryInvalid = "HDIFF_CompLZMA2DictionaryInvalid"; + public const string HDiffCompLZMA2NoCompressedPayload = "HDIFF_CompLZMA2NoCompressedPayload"; + public const string HDiffCompLZMADictionaryInvalidLength = "HDIFF_CompLZMADictionaryInvalidLength"; + public const string HDiffCompLZMASizeTooSmallForDictionaryRead = "HDIFF_CompLZMASizeToSmallForDictionaryRead"; +} + +/// Creates library-specific exceptions and translates them for unmanaged callers. +public static class ExceptionHelper +{ + internal static Exception? LastException; + + internal static NotSupportedException ThrowHDiffHeaderMagicNotSupported(ReadOnlySpan magic) + => new($"[{Const.HDiffHeaderMagicNotSupported}] Header magic: {magic.ToString()} is not supported!"); + internal static NotSupportedException ThrowHDiffHeaderCompressionNotSupported(ReadOnlySpan enumString) + => new($"[{Const.HDiffHeaderCompressionNotSupported}] HDIFF compression: {enumString.ToString()} is not supported!"); + internal static NotSupportedException ThrowHDiffHeaderChecksumNotSupported(ReadOnlySpan enumString) + => new($"[{Const.HDiffHeaderChecksumNotSupported}] HDIFF checksum: {enumString.ToString()} is not supported!"); + internal static NotSupportedException ThrowHDiffPatchFactoryNotSupported(HDiffMagic type) + => new($"[{Const.HDiffPatchFactoryNotSupported}] No factory supported for type: {type}!"); + internal static NullReferenceException ThrowHDiffInfoNotAllocated() + => new($"[{Const.HDiffInfoNotAllocated}] HDiffInfo pointer is not allocated!"); + internal static NullReferenceException ThrowHDiffInfoDirectoryPatchMetadataNotAllocated() + => new($"[{Const.HDiffInfoDirectoryPatchMetadataNotAllocated}] HDiffInfo directory patch metadata is not allocated!"); + internal static NullReferenceException ThrowHDiffInfoPatchMetadataNotAllocated() + => new($"[{Const.HDiffInfoPatchMetadataNotAllocated}] HDiffInfo patch metadata is not allocated!"); + internal static NullReferenceException ThrowHDiffFILEDescriptorNull() + => new($"[{Const.HDiffFILEDescriptorNull}] FILE descriptor cannot be null!"); + internal static NullReferenceException ThrowHDiffArgumentNull(string nameOfArg) + => new($"[{Const.HDiffArgumentNull}] Argument: {nameOfArg} cannot be null!"); + internal static InvalidOperationException ThrowHDiffHeaderSignatureEmptyOrUnreadable() + => new($"[{Const.HDiffHeaderSignatureEmptyOrUnreadable}] Header signature is empty or unreadable!"); + internal static EndOfStreamException ThrowHDiffEndOfFileOrData(Exception innerException) + => new($"[{Const.HDiffHeaderEndOfFileOrData}] HDiff Data has reached End-of-File or Data", innerException); + internal static InvalidOperationException ThrowHDiffPathIsEmptyOrInvalid() + => new($"[{Const.HDiffPathIsEmptyOrInvalid}] File or Directory path is empty or invalid"); + internal static IOException ThrowHDiffIOException(Exception ex) + => new($"[{Const.HDiffIOException}] An IO Error has occurred with message: {ex.Message}", ex); + internal static FileNotFoundException ThrowHDiffPatchInputPathNotExist(string filePath) + => new($"[{Const.HDiffPatchInputPathNotExist}] Input path does not exist: {filePath}", filePath); + internal static InvalidOperationException ThrowHDiffPatchInputSizeMismatched(string filePath, long existingSize, long expectingSize) + => new($"[{Const.HDiffPatchInputSizeMismatched}] Input file size does not match: {filePath} (Expecting: {expectingSize} bytes, but got: {existingSize} instead)."); + internal static InvalidOperationException ThrowHDiffPatchPathNotADirectory(string path) + => new($"[{Const.HDiffPatchPathNotADirectory}] Path is not a directory!: {path}"); + internal static InvalidOperationException ThrowHDiffPatchPathNotAFile(string path) + => new($"[{Const.HDiffPatchPathNotAFile}] Path is not a file!: {path}"); + internal static InvalidOperationException ThrowHDiffPatchInputFilesMismatched(long existingSize, long expectingSize) + => new($"[{Const.HDiffPatchInputFilesMismatched}] Input file size mismatched! Expecting: {expectingSize} bytes but got: {expectingSize} bytes instead."); + internal static InvalidOperationException ThrowHDiffPatchKuroInputFileSizeMismatched(string filePath, long existingSize, long expectingSize) + => new($"[{Const.HDiffPatchKuroInputFileSizeMismatched}] Kuro Games Patch input file size does not match: {filePath} (Expecting: {expectingSize} bytes, but got: {existingSize} instead)."); + internal static IndexOutOfRangeException ThrowHDiffStreamReadOutOfBound() + => new($"[{Const.HDiffStreamReadOutOfBound}] Stream Read is out of bound!"); + internal static InvalidDataException ThrowHDiffStringEncodingFailed(Exception? innerException) + => new($"[{Const.HDiffStringEncodingFailed}] Error while trying to encode a string!", innerException); + internal static InvalidDataException ThrowHDiffCompLZMAPropertyMissing() + => new($"[{Const.HDiffCompLZMAPropertyMissing}] The LZMA stream is missing its properties."); + internal static InvalidDataException ThrowHDiffCompLZMA2DictionaryInvalid(int property) + => new($"[{Const.HDiffCompLZMA2DictionaryInvalid}] The LZMA2 dictionary property must be at most 40, but was {property}."); + internal static InvalidDataException ThrowHDiffCompLZMA2NoCompressedPayload() + => new($"[{Const.HDiffCompLZMA2NoCompressedPayload}] The LZMA2 stream has no compressed payload."); + internal static InvalidDataException ThrowHDiffCompLZMADictionaryInvalidLength(int lzmaPropertySize, int property) + => new($"[{Const.HDiffCompLZMADictionaryInvalidLength}] The LZMA property length must be {lzmaPropertySize}, but was {property}."); + internal static InvalidDataException ThrowHDiffCompLZMASizeTooSmallForDictionaryRead() + => new($"[{Const.HDiffCompLZMASizeTooSmallForDictionaryRead}] The LZMA compressed size is too small to contain its headers and payload."); + + + /// Maps a library exception to the numeric code used by the unmanaged API. + /// The to record and translate, or for success. + /// 0 if is . A defined positive error code if the exception is recognized. Otherwise, -1. + public static int TryGetReturnCodeFromError(Exception? ex) + { + if (ex == null) + { + return 0; + } + + try + { + Interlocked.Exchange(ref LastException, ex); + + ReadOnlySpan message = ex.Message; + if (message.IsEmpty) + { + return -1; + } + + Span splitRanges = stackalloc Range[2]; + int splitLen = message.GetSplits(splitRanges, ' '); + + ReadOnlySpan splitFirst; + if (splitLen == 0 || + (splitFirst = message[splitRanges[0]])[0] != '[' || + splitFirst[^1] != ']') + { + return -1; + } + + splitFirst = splitFirst.Slice(1, message.Length - 2); +#if NET9_0_OR_GREATER + return Const.ExceptionToReturnCodeMap.GetAlternateLookup>() + .TryGetValue(splitFirst, out int returnCode) ? returnCode : -1; +#elif !NET6_0_OR_GREATER && NETSTANDARD + return splitFirst.ToString() is var splitFirstAsStr && + Const.ExceptionToReturnCodeMap.TryGetValue(splitFirstAsStr, out int value) ? value : -1; +#else + return Const.ExceptionToReturnCodeMap.GetValueOrDefault(splitFirst.ToString(), -1); +#endif + } + catch + { + return -1; + } + } + +#if NET8_0_OR_GREATER + /// Writes the most recently recorded error to a zero-terminated UTF-8 buffer. + /// The destination buffer, including space for a zero terminator. + /// The error details to include. + /// The number of bytes written. 0 when no error is available. Otherwise, -1 when is too small. + public static int TryGetLastErrorMessageUtf8(Span spanByte, LastErrorMessageType lastErrorMessageType) + { + Exception? thisException = Volatile.Read(ref LastException); + if (thisException == null || + spanByte.Length == 0) + { + return 0; + } + + spanByte[^1] = 0; + spanByte = spanByte[..^1]; // Slice and reserve last one byte for the null-terminator. + + StringBuilder builder = new(); + if (lastErrorMessageType.HasFlag(LastErrorMessageType.Message)) + { + builder.AppendLine(thisException.Message); + } + + if (lastErrorMessageType.HasFlag(LastErrorMessageType.StackTrace)) + { + builder.AppendLine(thisException.StackTrace); + } + + int written = 0; + foreach (ReadOnlyMemory chunk in builder.GetChunks()) + { + if (!Encoding.UTF8.TryGetBytes(chunk.Span, spanByte, out int thisWritten)) + { + return -1; + } + + written += thisWritten; + spanByte = spanByte[thisWritten..]; + } + + if (!spanByte.IsEmpty) spanByte[0] = 0; // Append null-terminator on last. + return written; + } + + /// Writes the most recently recorded error to a zero-terminated UTF-16 buffer. + /// The destination buffer, including space for a zero terminator. + /// The error details to include. + /// The number of characters written. 0 when no error is available. Otherwise, -1 when is too small. + public static int TryGetLastErrorMessageUnicode(Span spanChar, LastErrorMessageType lastErrorMessageType) + { + Exception? thisException = Volatile.Read(ref LastException); + if (thisException == null || + spanChar.Length == 0) + { + return 0; + } + + spanChar[^1] = '\0'; + spanChar = spanChar[..^1]; // Slice and reserve last one byte for the null-terminator. + + StringBuilder builder = new(); + if (lastErrorMessageType.HasFlag(LastErrorMessageType.Message)) + { + builder.AppendLine(thisException.Message); + } + + if (lastErrorMessageType.HasFlag(LastErrorMessageType.StackTrace)) + { + builder.AppendLine(thisException.StackTrace); + } + + int written = 0; + foreach (ReadOnlyMemory chunk in builder.GetChunks()) + { + if (!chunk.Span.TryCopyTo(spanChar)) + { + return -1; + } + + int thisWritten = chunk.Length; + written += thisWritten; + spanChar = spanChar[thisWritten..]; + } + + if (!spanChar.IsEmpty) spanChar[0] = '\0'; // Append null-terminator on last. + return written; + } + + /// Specifies which portions of the last error are returned to an unmanaged caller. + [Flags] + public enum LastErrorMessageType + { + /// Include the exception message. + Message = 1, + /// Include the exception stack trace. + StackTrace = 2, + /// Include both the exception message and stack trace. + MessageAndStackTrace = Message | StackTrace + } +#endif +} diff --git a/SharpHPatchZ/Extension/MemoryAlloc.cs b/SharpHPatchZ/Extension/MemoryAlloc.cs new file mode 100644 index 0000000..fef5d85 --- /dev/null +++ b/SharpHPatchZ/Extension/MemoryAlloc.cs @@ -0,0 +1,120 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SharpHPatchZ.Extension; + +internal static unsafe class MemoryAlloc +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T* Alloc(int elementCount = 1, bool initializeAlloc = false) + where T : unmanaged + { + if (typeof(T) == typeof(byte)) + { + return (T*)Alloc(elementCount, initializeAlloc); + } + + return (T*)Alloc(checked((long)elementCount * sizeof(T)), initializeAlloc); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void* Alloc(long byteCount, bool initializeAlloc = false) + { +#if NET6_0_OR_GREATER + return initializeAlloc ? + NativeMemory.AllocZeroed((nuint)byteCount) : + NativeMemory.Alloc((nuint)byteCount); +#else + var ptr = (void*)Marshal.AllocHGlobal((nint)byteCount); + if (!initializeAlloc) return ptr; + + Zero(ptr, byteCount); + return ptr; +#endif + } + + extension(ref T toCopy) where T : unmanaged + { + public T* CopyToUnmanagedUnsafe() + { + T* allocP = Alloc(); + ref T allocRef = ref allocP[0]; + allocRef = toCopy; + return allocP; + } + + public nint CopyToUnmanaged() => (nint)toCopy.CopyToUnmanagedUnsafe(); + public void CopyTo(ref T to) => to = toCopy; + public void CopyTo(void* to) => Unsafe.AsRef(to) = toCopy; + } + + public static void CopyTo(void* from, void* to) + where T : unmanaged + => Unsafe.AsRef(to) = Unsafe.AsRef(from); + + public static void CopyTo(void* from, ref T to) + where T : unmanaged + => to = Unsafe.AsRef(from); + + public static void Free(T* ptr) + where T : unmanaged + { + if (ptr == null) + { + return; + } + + Free((void*)ptr); + } + + public static void Free(void* ptr) + { + if (ptr == null) + { + return; + } + + ref byte asRef = ref Unsafe.AsRef(ptr); + asRef.TryDisposeIfMetadataType(); + + FreeRaw(ptr); + } + + public static void FreeRaw(void* ptr) + { + if (ptr == null) + { + return; + } + +#if NET6_0_OR_GREATER + NativeMemory.Free(ptr); +#else + Marshal.FreeHGlobal((nint)ptr); +#endif + } + +#if !NET7_0_OR_GREATER + public static void Zero(void* ptr, long byteCount) + { + ref byte current = ref Unsafe.AsRef(ptr); + while (byteCount >= sizeof(nuint)) + { + Unsafe.As(ref current) = 0; + + current = ref Unsafe.Add(ref current, sizeof(nuint)); + byteCount -= sizeof(nuint); + } + + while (byteCount != 0) + { + current = 0; + current = ref Unsafe.Add(ref current, 1); + --byteCount; + } + } +#else + public static void Zero(void* ptr, long byteCount) + => NativeMemory.Fill(ptr, (nuint)byteCount, 0); +#endif +} diff --git a/SharpHPatchZ/Extension/MetadataExtension.cs b/SharpHPatchZ/Extension/MetadataExtension.cs new file mode 100644 index 0000000..fbd0539 --- /dev/null +++ b/SharpHPatchZ/Extension/MetadataExtension.cs @@ -0,0 +1,75 @@ +using System; +using System.Runtime.CompilerServices; +using SharpHPatchZ.Header.Metadata; + +namespace SharpHPatchZ.Extension; + +internal static class MetadataExtension +{ + public static unsafe bool TryGetMetadataType(void* metadataP, out MetadataTypeConst metadataType) + { + if (metadataP == null) + { + metadataType = default; + return false; + } + + ref MetadataTypeConst metadataTypeCopy = ref Unsafe.AsRef(metadataP); + metadataType = metadataTypeCopy; + + return IsTypeDefined(metadataType); + } + + extension(ref T metadata) where T : unmanaged + { + public bool TryGetMetadataType(out MetadataTypeConst metadataType) + { + ref MetadataTypeConst metadataTypeCopy = ref Unsafe.As(ref metadata); + metadataType = metadataTypeCopy; + + return IsTypeDefined(metadataType); + } + + public bool TryDisposeIfMetadataType() + { + if (!metadata.TryGetMetadataType(out MetadataTypeConst metadataType)) + { + return false; + } + + switch (metadataType) + { + case MetadataTypeConst.ChecksumDataInfoType: + Unsafe.As(ref metadata).Dispose(); + break; + case MetadataTypeConst.DirectoryPatchMetadataType: + Unsafe.As(ref metadata).Dispose(); + break; + case MetadataTypeConst.PatchMetadataType: + Unsafe.As(ref metadata).Dispose(); + break; + case MetadataTypeConst.UnmanagedArrayType: + Unsafe.As>(ref metadata).Dispose(); + break; + case MetadataTypeConst.Utf16UnmanagedStringType: + Unsafe.As(ref metadata).Dispose(); + break; + case MetadataTypeConst.IsMetadataType: + default: + return false; + } + + return true; + } + } + + public static bool IsTypeDefined(MetadataTypeConst metadataType) + { +#if NET6_0_OR_GREATER + return Enum.IsDefined(metadataType) +#else + return Enum.IsDefined(typeof(MetadataTypeConst), metadataType) +#endif + && metadataType.HasFlag(MetadataTypeConst.IsMetadataType); + } +} diff --git a/SharpHPatchZ/Extension/NativeMemoryBuffer.cs b/SharpHPatchZ/Extension/NativeMemoryBuffer.cs new file mode 100644 index 0000000..2fa1e19 --- /dev/null +++ b/SharpHPatchZ/Extension/NativeMemoryBuffer.cs @@ -0,0 +1,82 @@ +using System; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace SharpHPatchZ.Extension; + +internal sealed unsafe class NativeMemoryBuffer : IDisposable + where T : unmanaged +{ + private readonly int _length; + private long _pointer; + + public NativeMemoryBuffer(int length) + { + if (length < 0) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + _length = length; + if (length == 0) + { + return; + } + + _pointer = (nint)MemoryAlloc.Alloc(length); + if (_pointer == 0) + { + throw new OutOfMemoryException(); + } + } + + ~NativeMemoryBuffer() + => Release(); + + public int Length => _length; + + public Span Span + { + get + { + nint pointer = (nint)_pointer; + if (pointer == 0) + { + return _length == 0 + ? Span.Empty + : throw new ObjectDisposedException(nameof(NativeMemoryBuffer)); + } + + return new Span((void*)pointer, _length); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref T GetReference() + { + nint pointer = (nint)_pointer; + if (pointer == 0) + { + throw new ObjectDisposedException(nameof(NativeMemoryBuffer)); + } + + return ref Unsafe.AsRef((void*)pointer); + } + + public void Dispose() + { + Release(); + GC.SuppressFinalize(this); + } + + private void Release() + { + nint pointer = (nint)Interlocked.Exchange(ref _pointer, 0); + if (pointer == 0) + { + return; + } + + MemoryAlloc.FreeRaw((void*)pointer); + } +} diff --git a/SharpHPatchZ/Extension/PathHelper.cs b/SharpHPatchZ/Extension/PathHelper.cs new file mode 100644 index 0000000..09f2834 --- /dev/null +++ b/SharpHPatchZ/Extension/PathHelper.cs @@ -0,0 +1,29 @@ +using System; +using System.IO; + +namespace SharpHPatchZ.Extension; + +internal static class PathHelper +{ + extension(string path) + { + public DirectoryInfo GetDirectoryInfo() + => new FileInfo(path).Exists + ? throw ExceptionHelper.ThrowHDiffPatchPathNotADirectory(path) + : new DirectoryInfo(path); + + public FileInfo GetFileInfo() + => new DirectoryInfo(path).Exists + ? throw ExceptionHelper.ThrowHDiffPatchPathNotAFile(path) + : new FileInfo(path); + + public bool IsDirectory() + { + ReadOnlySpan pathSpan = path; + if (pathSpan.IsEmpty) return false; + + char lastChar = pathSpan[^1]; + return lastChar is '\\' or '/'; + } + } +} diff --git a/SharpHPatchZ/Extension/StreamExtension.cs b/SharpHPatchZ/Extension/StreamExtension.cs new file mode 100644 index 0000000..1073f01 --- /dev/null +++ b/SharpHPatchZ/Extension/StreamExtension.cs @@ -0,0 +1,415 @@ +using System; +using System.Buffers; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpHPatchZ.Header.Metadata; +using SharpHPatchZ.IO.Reader; + +namespace SharpHPatchZ.Extension; + +internal static class StreamExtension +{ + extension(Stream stream) + { +#if NET6_0 + public async ValueTask ReadExactlyAsync( + Memory buffer, + CancellationToken cancellationToken = default) + { + await stream.ReadAtLeastAsync(buffer, buffer.Length, true, cancellationToken); + } +#endif + + public void ReadExactly( + byte[] buffer, + int offset, + int count) + { +#if !NET6_0_OR_GREATER + _ = stream.ReadAtLeast(buffer, offset, count, count, true); +#else + _ = stream.ReadAtLeast(buffer.AsSpan(offset, count), count); +#endif + } + + public async ValueTask ReadExactlyAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default) + { +#if !NET6_0_OR_GREATER + ValueTask vt = stream.ReadAtLeastAsync(buffer, offset, count, count, true, cancellationToken); + await vt; +#else + await stream.ReadAtLeastAsync(buffer.AsMemory(offset, count), count, true, cancellationToken); +#endif + } + +#if !NET6_0_OR_GREATER + public int ReadAtLeast( + byte[] buffer, + int offset, + int count, + int minimumBytes, + bool throwOnEndOfStream = false) + { + Debug.Assert(minimumBytes <= buffer.Length); + + int totalRead = offset; + while (totalRead < minimumBytes) + { + int read = stream.Read(buffer, offset + totalRead, count - totalRead); + if (read == 0) + { + return throwOnEndOfStream ? throw new EndOfStreamException() : totalRead; + } + + totalRead += read; + } + + return totalRead; + } + + public async ValueTask ReadAtLeastAsync( + byte[] buffer, + int offset, + int count, + int minimumBytes, + bool throwOnEndOfStream = false, + CancellationToken cancellationToken = default) + { + Debug.Assert(minimumBytes <= buffer.Length); + + int totalRead = offset; + while (totalRead < minimumBytes) + { + int read = await stream.ReadAsync(buffer, offset + totalRead, count - totalRead, cancellationToken).ConfigureAwait(false); + if (read == 0) + { + return throwOnEndOfStream ? throw new EndOfStreamException() : totalRead; + } + + totalRead += read; + } + + return totalRead; + } +#else + public int ReadAtLeast( + Span buffer, + int minimumBytes, + bool throwOnEndOfStream = false) + { + Debug.Assert(minimumBytes <= buffer.Length); + + int totalRead = 0; + while (totalRead < minimumBytes) + { + int read = stream.Read(buffer[totalRead..]); + if (read == 0) + { + return throwOnEndOfStream ? throw new EndOfStreamException() : totalRead; + } + + totalRead += read; + } + + return totalRead; + } + + public async ValueTask ReadAtLeastAsync( + Memory buffer, + int minimumBytes, + bool throwOnEndOfStream = false, + CancellationToken cancellationToken = default) + { + Debug.Assert(minimumBytes <= buffer.Length); + + int totalRead = 0; + while (totalRead < minimumBytes) + { + int read = await stream.ReadAsync(buffer[totalRead..], cancellationToken).ConfigureAwait(false); + if (read == 0) + { + return throwOnEndOfStream ? throw new EndOfStreamException() : totalRead; + } + + totalRead += read; + } + + return totalRead; + } +#endif + } + + public static unsafe UnmanagedArray* CreateUnmanagedStringList( + scoped ReadOnlySpan buffer, int count) + { + UnmanagedArray* unmanagedStringArray = UnmanagedArray.CreateAllocUnsafe(count); + Span unmanagedStringSpan = unmanagedStringArray->GetSpan(); + int index = 0; + do + { + int indexOfNull = buffer.IndexOf((byte)0); + if (indexOfNull < 0) + { + throw ExceptionHelper.ThrowHDiffStreamReadOutOfBound(); + } + + ReadOnlySpan currentSlice = buffer[..indexOfNull]; + unmanagedStringSpan[index++] = Utf16UnmanagedString.CreateFromManaged(currentSlice); + + buffer = buffer[(indexOfNull + 1)..]; + } while (!buffer.IsEmpty); + + return index != count + ? throw ExceptionHelper.ThrowHDiffStreamReadOutOfBound() + : unmanagedStringArray; + } + + extension(BittableStreamReader reader) + { + public async ValueTask CreateUnmanagedStringListAsync(int count, int bufferSize, CancellationToken token) + { + if (count == 0) + { + return 0; + } + + long readerOffset = reader.Offset; + byte[] buffer = ArrayPool.Shared.Rent(bufferSize); + + try + { + // Preload string buffer + await reader.ReadBytesAsync(buffer.AsMemory(0, bufferSize), token); + unsafe + { + return reader.Offset != readerOffset + bufferSize + ? throw ExceptionHelper.ThrowHDiffStreamReadOutOfBound() + : (nint)CreateUnmanagedStringList(buffer.AsSpan(0, bufferSize), count); + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public unsafe UnmanagedArray* CreateUnmanagedStringList(int count, int bufferSize) + { + if (count == 0) + { + return null; + } + + long readerOffset = reader.Offset; + + byte[]? buffer = bufferSize > 4 << 10 ? ArrayPool.Shared.Rent(bufferSize) : null; + Span bufferSpan = (buffer ?? stackalloc byte[bufferSize])[..bufferSize]; + + try + { + // Preload string buffer + reader.ReadBytes(bufferSpan); + return reader.Offset != readerOffset + bufferSize + ? throw ExceptionHelper.ThrowHDiffStreamReadOutOfBound() + : CreateUnmanagedStringList(bufferSpan, count); + } + finally + { + if (buffer != null) ArrayPool.Shared.Return(buffer); + } + } + + public async ValueTask CreateUnmanagedInt64ListAsync(int count, CancellationToken token) + { + if (count == 0) + { + return 0; + } + + long[] array = ArrayPool.Shared.Rent(count); + try + { + for (int i = 0; i < count; i++) + { + array[i] = await reader.ReadLong7BitAsync(token); + } + + unsafe + { + UnmanagedArray* allocArray = UnmanagedArray.CreateAllocUnsafe(count); + array.AsSpan(0, count).CopyTo(allocArray->GetSpan()); + return (nint)allocArray; + } + } + finally + { + ArrayPool.Shared.Return(array); + } + } + + public unsafe UnmanagedArray* CreateUnmanagedInt64List(int count) + { + if (count == 0) + { + return null; + } + + UnmanagedArray* allocArray = UnmanagedArray.CreateAllocUnsafe(count); + Span allocSpan = allocArray->GetSpan(); + + for (int i = 0; i < count; i++) + { + allocSpan[i] = reader.ReadLong7Bit(); + } + + return allocArray; + } + + public async ValueTask CreateUnmanagedInt64As32ListAsync(int count, CancellationToken token) + { + if (count == 0) + { + return 0; + } + + int[] array = ArrayPool.Shared.Rent(count); + try + { + long backNumber = -1; + for (int i = 0; i < count; i++) + { + array[i] = (int)(backNumber += 1 + await reader.ReadLong7BitAsync(token)); + } + + unsafe + { + UnmanagedArray* allocArray = UnmanagedArray.CreateAllocUnsafe(count); + array.AsSpan(0, count).CopyTo(allocArray->GetSpan()); + return (nint)allocArray; + } + } + finally + { + ArrayPool.Shared.Return(array); + } + } + + public unsafe UnmanagedArray* CreateUnmanagedInt64As32List(int count) + { + if (count == 0) + { + return null; + } + + UnmanagedArray* allocArray = UnmanagedArray.CreateAllocUnsafe(count); + Span allocSpan = allocArray->GetSpan(); + + long backNumber = -1; + for (int i = 0; i < count; i++) + { + allocSpan[i] = (int)(backNumber += 1 + reader.ReadLong7Bit()); + } + + return allocArray; + } + + public async ValueTask CreateUnmanagedIndexPairListAsync(int count, CancellationToken token) + { + if (count == 0) + { + return 0; + } + + FileIndexPair[] array = ArrayPool.Shared.Rent(count); + try + { + long oldIndexNumber = -1; + long newIndexNumber = -1; + for (int i = 0; i < count; i++) + { + oldIndexNumber += 1 + await reader.ReadLong7BitAsync(token); + newIndexNumber += 1 + await reader.ReadLong7BitAsync(token); + array[i] = new FileIndexPair + { + OldIndex = (int)oldIndexNumber, + NewIndex = (int)newIndexNumber + }; + } + + unsafe + { + FileIndexPair* alloc = MemoryAlloc.Alloc(count); + array.AsSpan(0, count).CopyTo(new Span(alloc, count)); + return (nint)alloc; + } + } + finally + { + ArrayPool.Shared.Return(array); + } + } + + public unsafe FileIndexPair* CreateUnmanagedIndexPairList(int count) + { + if (count == 0) + { + return null; + } + + FileIndexPair* alloc = MemoryAlloc.Alloc(count); + Span allocSpan = new(alloc, count); + long oldIndexNumber = -1; + long newIndexNumber = -1; + for (int i = 0; i < count; i++) + { + long incNewValue = reader.ReadLong7Bit(); + newIndexNumber += 1 + incNewValue; + + long incOldValue = reader.ReadLong7Bit(1); + int pSign = reader.PreviousByte; + + if (pSign >> (8 - 1) == 0) + oldIndexNumber += 1 + incOldValue; + else + oldIndexNumber = oldIndexNumber + 1 - incOldValue; + + allocSpan[i].OldIndex = (int)oldIndexNumber; + allocSpan[i].NewIndex = (int)newIndexNumber; + } + + return alloc; + } + + public async ValueTask AdvanceSeekToAsync(int advancedBytes, CancellationToken token) + { + byte[] buffer = ArrayPool.Shared.Rent(advancedBytes); + try + { + await reader.ReadBytesAsync(buffer.AsMemory(0, advancedBytes), token); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public void AdvanceSeekTo(int advancedBytes) + { + byte[] buffer = ArrayPool.Shared.Rent(advancedBytes); + try + { + reader.ReadBytes(buffer.AsSpan(0, advancedBytes)); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + } +} diff --git a/SharpHPatchZ/Extension/StringExtension.cs b/SharpHPatchZ/Extension/StringExtension.cs new file mode 100644 index 0000000..1563f64 --- /dev/null +++ b/SharpHPatchZ/Extension/StringExtension.cs @@ -0,0 +1,182 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace SharpHPatchZ.Extension; + +internal static class StringExtension +{ + public static int GetSplits( + this ReadOnlySpan span, + Span ranges, + char separator, + StringSplitOptions splitOptions = StringSplitOptions.None) + { +#if NET8_0_OR_GREATER + return span.Split(ranges, + separator, + splitOptions); +#else + if (ranges.Length == 0) + { + return 0; + } + +#if NET6_0_OR_GREATER + bool isTrimEntries = splitOptions.HasFlag(StringSplitOptions.TrimEntries); +#else + bool isTrimEntries = false; +#endif + bool isRemoveEmpty = splitOptions.HasFlag(StringSplitOptions.RemoveEmptyEntries); + + int rangeCount = 0; + int startInclusive = 0; + + if (ranges.Length > 1) + { + while (true) + { + int separatorOffset = span[startInclusive..].IndexOf(separator); + if (separatorOffset < 0) + { + break; + } + + int endExclusive = startInclusive + separatorOffset; + int untrimmedEndExclusive = endExclusive; + + if (isTrimEntries) + { + TrimSplitEntry(span, ref startInclusive, ref endExclusive); + } + + if (isRemoveEmpty && + startInclusive == endExclusive) + { + continue; + } + + if (rangeCount >= ranges.Length - 1) + { + break; + } + + ranges[rangeCount++] = new Range(startInclusive, endExclusive); + + startInclusive = untrimmedEndExclusive + 1; + } + } + + int remainderEndExclusive = span.Length; + if (isTrimEntries) + { + TrimSplitEntry(span, ref startInclusive, ref remainderEndExclusive); + } + if (startInclusive != remainderEndExclusive) + { + ranges[rangeCount++] = new Range(startInclusive, remainderEndExclusive); + } + + return rangeCount; +#endif + } + +#if !NET8_0_OR_GREATER + private static void TrimSplitEntry( + ReadOnlySpan source, + ref int startInclusive, + ref int endExclusive) + { + while (startInclusive < endExclusive && char.IsWhiteSpace(source[startInclusive])) + { + startInclusive++; + } + + while (endExclusive > startInclusive && char.IsWhiteSpace(source[endExclusive - 1])) + { + endExclusive--; + } + } +#endif + +#if NET6_0_OR_GREATER + public static unsafe NativeStringEncoding GuessStringEncoding( + void* stringP, + nuint maxBytes) + { + byte* ptr = (byte*)stringP; + + if (ptr == null) + throw new ArgumentNullException(nameof(ptr)); + + if (maxBytes >= 2) + { + // UTF-16 LE BOM + if (ptr[0] == 0xFF && ptr[1] == 0xFE) + return NativeStringEncoding.Unicode; + + // UTF-8 BOM + if (maxBytes >= 3 && + ptr[0] == 0xEF && + ptr[1] == 0xBB && + ptr[2] == 0xBF) + { + return NativeStringEncoding.Utf8; + } + } + + // Heuristic: + // ASCII text encoded as UTF-16LE tends to have NUL bytes + // in every odd byte. + nuint pairs = 0; + nuint zeroHighBytes = 0; + + nuint checkLength = Math.Min(maxBytes, 64u); + + for (nuint i = 0; i + 1 < checkLength; i += 2) + { + byte lo = ptr[i]; + byte hi = ptr[i + 1]; + + // UTF-16 terminator + if (lo == 0 && hi == 0) + break; + + pairs++; + + if (hi == 0) + zeroHighBytes++; + } + + if (pairs != 0 && + zeroHighBytes * 4 >= pairs * 3) // >= 75% + { + return NativeStringEncoding.Unicode; + } + + return NativeStringEncoding.Utf8; + } + + public static unsafe string? GetManagedStringAuto(void* ptr) + { + if (ptr == null) return null; + + NativeStringEncoding encoding = GuessStringEncoding(ptr, 8); + return encoding switch + { + NativeStringEncoding.Unicode => MemoryMarshal.CreateReadOnlySpanFromNullTerminated((char*)ptr).ToString(), + NativeStringEncoding.Utf8 => Encoding.UTF8.GetString(MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)ptr)), + _ => null + }; + } +#endif +} + +/// Identifies an encoding used by a zero-terminated native character sequence. +public enum NativeStringEncoding +{ + /// UTF-8 encoding. + Utf8, + /// UTF-16 encoding in the platform's native little-endian representation. + Unicode +} diff --git a/SharpHPatchZ/HPatch.UnmanagedExtern.cs b/SharpHPatchZ/HPatch.UnmanagedExtern.cs new file mode 100644 index 0000000..0cf420d --- /dev/null +++ b/SharpHPatchZ/HPatch.UnmanagedExtern.cs @@ -0,0 +1,324 @@ +using System.IO; +using System.Threading.Tasks; + +#if NET8_0_OR_GREATER +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; +using SharpHPatchZ.Extension; +using SharpHPatchZ.Header; +using SharpHPatchZ.Header.Metadata; +using SharpHPatchZ.Native; +#endif + +// ReSharper disable InconsistentNaming +// ReSharper disable IdentifierTypo +// ReSharper disable StringLiteralTypo + +#if NET8_0_OR_GREATER +#if USEWINDOWS +using ConventionCall = System.Runtime.CompilerServices.CallConvStdcall; +#else +using ConventionCall = System.Runtime.CompilerServices.CallConvCdecl; +#endif +#endif + +namespace SharpHPatchZ; + +public static partial class HPatch +{ +#if NET8_0_OR_GREATER + /// Parses a zero-terminated UTF-8 or UTF-16 patch signature for an unmanaged caller. + /// A pointer to the zero-terminated signature . + /// A pointer that receives the . + /// A pointer that receives the . + /// A pointer that receives the . + /// 0 if is parsed successfully. Otherwise, a library error code. + [UnmanagedCallersOnly(CallConvs = [typeof(ConventionCall)], EntryPoint = "shpz_read_header_signature_string")] + public static unsafe int SharpHPatchZ_ReadHeaderSignatureStringAuto(void* signatureP, HDiffMagic* magicTypeP, HDiffCompression* compressionTypeP, HDiffChecksum* checksumTypeP) + { + char[]? signatureWideBuffer = null; + try + { + string? signature = StringExtension.GetManagedStringAuto(signatureP); + + ref HDiffMagic magicTypeRef = ref magicTypeP[0]; + ref HDiffCompression compressionTypeRef = ref compressionTypeP[0]; + ref HDiffChecksum checksumTypeRef = ref checksumTypeP[0]; + + HeaderReader.ReadBasicHeaderSignature(signature, + out magicTypeRef, + out compressionTypeRef, + out checksumTypeRef); + return 0; + } + catch (Exception ex) + { + return ExceptionHelper.TryGetReturnCodeFromError(ex); + } + finally + { + if (signatureWideBuffer != null) ArrayPool.Shared.Return(signatureWideBuffer); + } + } + + /// Initializes an from an unmanaged memory buffer. + /// A pointer to the patch data. + /// The patch-data length, in bytes. + /// A pointer that receives the initialized . + /// A pointer to , or to use defaults. + /// 0 if initialization succeeds. Otherwise, a library error code. + [UnmanagedCallersOnly(CallConvs = [typeof(ConventionCall)], EntryPoint = "shpz_init_from_memory")] + public static unsafe int SharpHPatchZ_InitializeFromMemory(byte* dataP, long dataLength, HDiffInfo* signP, InitializeOptions* initializeOptionsP) + { + try + { + InitializeOptions initializeOptions = initializeOptionsP != null ? *initializeOptionsP : default; + HDiffInfo thisInfo = CreateInstance(CreateUnmanagedStreamWrapper, initializeOptions); + thisInfo.CopyTo(signP); + + return 0; + } + catch (Exception ex) + { + return ExceptionHelper.TryGetReturnCodeFromError(ex); + } + + (Stream Stream, bool LeaveOpen) CreateUnmanagedStreamWrapper(long position) + { + UnmanagedMemoryStream stream = new(dataP, dataLength); + stream.Position = position; + return (stream, false); + } + } + + /// Initializes an from a zero-terminated UTF-8 or UTF-16 file path. + /// A pointer to the zero-terminated patch path. + /// A pointer that receives the initialized . + /// A pointer to , or to use defaults. + /// 0 if initialization succeeds. Otherwise, a library error code. + [UnmanagedCallersOnly(CallConvs = [typeof(ConventionCall)], EntryPoint = "shpz_init_from_filepath")] + public static unsafe int SharpHPatchZ_InitializeFromFilePathAuto(void* pathP, HDiffInfo* signP, InitializeOptions* initializeOptionsP) + { + try + { + string? filePath = StringExtension.GetManagedStringAuto(pathP); + if (filePath == null) + { + throw ExceptionHelper.ThrowHDiffPathIsEmptyOrInvalid(); + } + + InitializeOptions initializeOptions = initializeOptionsP != null ? *initializeOptionsP : default; + HDiffInfo thisInfo = CreateInstance(pos => CreateFileStreamWrapper(filePath, pos), initializeOptions); + thisInfo.CopyTo(signP); + + return 0; + } + catch (Exception ex) + { + return ExceptionHelper.TryGetReturnCodeFromError(ex); + } + } + + /// Initializes an from a native FILE descriptor. + /// A pointer to the native FILE descriptor. + /// A pointer that receives the initialized . + /// A pointer to , or to use defaults. + /// 0 if initialization succeeds. Otherwise, a library error code. + [UnmanagedCallersOnly(CallConvs = [typeof(ConventionCall)], EntryPoint = "shpz_init_from_FILE")] + public static unsafe int SharpHPatchZ_InitializeFromFILE(void* FILEP, HDiffInfo* signP, InitializeOptions* initializeOptionsP) + { + try + { + if (FILEP == null) + { + throw ExceptionHelper.ThrowHDiffFILEDescriptorNull(); + } + + InitializeOptions initializeOptions = initializeOptionsP != null ? *initializeOptionsP : default; + HDiffInfo thisInfo = CreateInstance(pos => CreateFileStreamWrapper(FILEP, pos), initializeOptions); + thisInfo.CopyTo(signP); + + return 0; + } + catch (Exception ex) + { + return ExceptionHelper.TryGetReturnCodeFromError(ex); + } + } + + /// Applies a patch identified by zero-terminated UTF-8 or UTF-16 file-system paths. + /// A pointer to the zero-terminated patch-file path. + /// A pointer to the zero-terminated input path. + /// A pointer to the zero-terminated output path. + /// A pointer to an initialized . + /// A pointer to , or to use defaults. + /// A pointer to a , or for no callback. + /// 0 if patching succeeds. Otherwise, a library error code. + [UnmanagedCallersOnly(CallConvs = [typeof(ConventionCall)], EntryPoint = "shpz_patch_from_filepath")] + public static unsafe int SharpHPatchZ_PatchFromFilePathAuto( + void* patchPathP, + void* inputPathP, + void* outputPathP, + HDiffInfo* infoP, + PatchOptions* optionsP, + ProgressCallback* progressCallbackP) + { + string? patchPath = StringExtension.GetManagedStringAuto(patchPathP); + string? inputPath = StringExtension.GetManagedStringAuto(inputPathP); + string? outputPath = StringExtension.GetManagedStringAuto(outputPathP); + + try + { + if (patchPath == null) throw ExceptionHelper.ThrowHDiffArgumentNull(nameof(patchPathP)); + if (inputPath == null) throw ExceptionHelper.ThrowHDiffArgumentNull(nameof(inputPathP)); + if (outputPath == null) throw ExceptionHelper.ThrowHDiffArgumentNull(nameof(outputPathP)); + if (infoP == null) throw ExceptionHelper.ThrowHDiffInfoNotAllocated(); + + PatchOptions options = optionsP == null ? new PatchOptions() : Unsafe.AsRef(optionsP); // Copy + ProgressCallback progressCallback = progressCallbackP == null ? new ProgressCallback() : Unsafe.AsRef(progressCallbackP); + ref HDiffInfo info = ref infoP[0]; + + return Patch(info, + pos => CreateFileStreamWrapper(patchPath, pos), + inputPath, + outputPath, + options, + progressCallback); + } + catch (Exception ex) + { + return ExceptionHelper.TryGetReturnCodeFromError(ex); + } + } + + /// Applies a patch read from a native FILE descriptor. + /// A pointer to the native patch-file descriptor. + /// A pointer to the zero-terminated input path. + /// A pointer to the zero-terminated output path. + /// A pointer to an initialized . + /// A pointer to , or to use defaults. + /// A pointer to a , or for no callback. + /// 0 if patching succeeds. Otherwise, a library error code. + [UnmanagedCallersOnly(CallConvs = [typeof(ConventionCall)], EntryPoint = "shpz_patch_from_FILE")] + public static unsafe int SharpHPatchZ_PatchFromFILE( + void* FILEP, + void* inputPathP, + void* outputPathP, + HDiffInfo* infoP, + PatchOptions* optionsP, + ProgressCallback* progressCallbackP) + { + string? inputPath = StringExtension.GetManagedStringAuto(inputPathP); + string? outputPath = StringExtension.GetManagedStringAuto(outputPathP); + + try + { + if (FILEP == null) throw ExceptionHelper.ThrowHDiffArgumentNull(nameof(FILEP)); + if (inputPath == null) throw ExceptionHelper.ThrowHDiffArgumentNull(nameof(inputPathP)); + if (outputPath == null) throw ExceptionHelper.ThrowHDiffArgumentNull(nameof(outputPathP)); + if (infoP == null) throw ExceptionHelper.ThrowHDiffInfoNotAllocated(); + + PatchOptions options = optionsP == null ? new PatchOptions() : Unsafe.AsRef(optionsP); // Copy + ProgressCallback progressCallback = progressCallbackP == null ? new ProgressCallback() : Unsafe.AsRef(progressCallbackP); + ref HDiffInfo info = ref infoP[0]; + + return Patch(info, + pos => CreateFileStreamWrapper(FILEP, pos), + inputPath, + outputPath, + options, + progressCallback); + } + catch (Exception ex) + { + return ExceptionHelper.TryGetReturnCodeFromError(ex); + } + } + + /// Releases metadata allocated for an . + /// A pointer to the to release. + /// 0 if the metadata is released. Otherwise, a library error code. + [UnmanagedCallersOnly(CallConvs = [typeof(ConventionCall)], EntryPoint = "shpz_free_diff_info")] + public static unsafe int SharpHPatchZ_FreeDiffInfo(HDiffInfo* ptr) + { + try + { + if (ptr == null) + { + throw ExceptionHelper.ThrowHDiffInfoNotAllocated(); + } + + MemoryAlloc.Free(ptr->MetadataP); + return 0; + } + catch (Exception ex) + { + return ExceptionHelper.TryGetReturnCodeFromError(ex); + } + } + + /// Gets the associated with an . + /// A pointer to an initialized . + /// A pointer to the . + [UnmanagedCallersOnly(CallConvs = [typeof(ConventionCall)], EntryPoint = "shpz_util_get_patch_metadata")] + public static unsafe PatchMetadata* SharpHPatchZ_TryGetPatchMetadata(HDiffInfo* infoP) + => (PatchMetadata*)Unsafe.AsPointer(ref infoP[0].GetPatchMetadata()); + + /// Gets the associated with an . + /// A pointer to an initialized . + /// A pointer to the , or when it is unavailable. + [UnmanagedCallersOnly(CallConvs = [typeof(ConventionCall)], EntryPoint = "shpz_util_get_directory_patch_metadata")] + public static unsafe DirectoryPatchMetadata* SharpHPatchZ_TryGetDirectoryPatchMetadata(HDiffInfo* infoP) + => (DirectoryPatchMetadata*)Unsafe.AsPointer(ref infoP[0].MetadataAs()); + + /// Writes the last recorded error to a zero-terminated UTF-8 buffer. + /// A pointer to the destination buffer. + /// The buffer length, in bytes. + /// The details to include. + /// The number of bytes written. Otherwise, -1 if is too small. + [UnmanagedCallersOnly(CallConvs = [typeof(ConventionCall)], EntryPoint = "shpz_get_last_errorA")] + public static unsafe int SharpHPatchZ_GetLastErrorUtf8(byte* bufferA, int bufferLength, ExceptionHelper.LastErrorMessageType messageType) + => ExceptionHelper.TryGetLastErrorMessageUtf8(new Span(bufferA, bufferLength), messageType); + + /// Writes the last recorded error to a zero-terminated UTF-16 buffer. + /// A pointer to the destination buffer. + /// The buffer length, in characters. + /// The details to include. + /// The number of characters written. Otherwise, -1 if is too small. + [UnmanagedCallersOnly(CallConvs = [typeof(ConventionCall)], EntryPoint = "shpz_get_last_errorW")] + public static unsafe int SharpHPatchZ_GetLastErrorUnicode(char* bufferW, int bufferLength, ExceptionHelper.LastErrorMessageType messageType) + => ExceptionHelper.TryGetLastErrorMessageUnicode(new Span(bufferW, bufferLength), messageType); + + private static unsafe (Stream Stream, bool LeaveOpen) CreateFileStreamWrapper(void* FILEP, long position) + { + try + { + SafeFileHandle fileHandle = PInvoke.GetSafeFileHandleFromFILE(FILEP); + FileStream stream = new(fileHandle, FileAccess.Read); + stream.Position = position; + return (stream, true); + } + catch (IOException ex) + { + throw ExceptionHelper.ThrowHDiffIOException(ex); + } + } +#endif + + private static (Stream Stream, bool LeaveOpen) CreateFileStreamWrapper(string filePath, long position) + { + FileStream stream = new(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + stream.Position = position; + return (stream, false); + } + + private static ValueTask<(Stream Stream, bool LeaveOpen)> CreateFileStreamWrapperAsync(string filePath, long position) + { + FileStream stream = new(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + stream.Position = position; + return new ValueTask<(Stream Stream, bool LeaveOpen)>((stream, false)); + } +} diff --git a/SharpHPatchZ/HPatch.Utility.cs b/SharpHPatchZ/HPatch.Utility.cs new file mode 100644 index 0000000..3e6b922 --- /dev/null +++ b/SharpHPatchZ/HPatch.Utility.cs @@ -0,0 +1,197 @@ +using SharpHPatchZ.Header; +using SharpHPatchZ.Header.Metadata; +using System; +using System.IO; +using System.Runtime.CompilerServices; +using SharpHPatchZ.Extension; + +namespace SharpHPatchZ; + +public static partial class HPatch +{ + /// A context and information struct for the to be retrieved from. + extension(ref HDiffInfo info) + { + /// + /// Try retrieve a struct from a patch context. + /// + /// A retrieved struct of containing the main information about the patch file. + /// + /// Returns if is successfully retrieved. Otherwise, if the context struct is invalid or corrupted. + /// + public bool TryGetPatchMetadata(out PatchMetadata patchMetadata) + { + Unsafe.SkipInit(out patchMetadata); + + ref PatchMetadata patchMetadataRef = ref info.GetPatchMetadata(); + if (Unsafe.IsNullRef(ref patchMetadataRef)) + { + return false; + } + + patchMetadata = patchMetadataRef; + return true; + } + + /// + /// Try retrieve a struct from a patch context. + /// + /// A retrieved struct of containing the main information about the patch file. + /// + /// Returns if is successfully retrieved. + /// Otherwise, if the patch context does not contain struct. + /// + public bool TryGetDirectoryPatchMetadata(out DirectoryPatchMetadata patchMetadata) + { + Unsafe.SkipInit(out patchMetadata); + + ref DirectoryPatchMetadata patchMetadataRef = ref info.MetadataAs(); + if (Unsafe.IsNullRef(ref patchMetadataRef)) + { + return false; + } + + patchMetadata = patchMetadataRef; + return true; + } + + /// + /// Try retrieves both total Input and Output size from a patch context. + /// + /// The total size of an Input File/Directory. + /// The total size of an Output File/Directory. + /// + /// Returns if both and are successfully retrieved. + /// Otherwise, if the patch context is invalid or corrupted. + /// + public bool TryGetDiffSizeInfo(out long totalInputSize, + out long totalOutputSize) + { + Unsafe.SkipInit(out totalInputSize); + Unsafe.SkipInit(out totalOutputSize); + + ref PatchMetadata patchMetadata = ref info.GetPatchMetadata(); + if (Unsafe.IsNullRef(ref patchMetadata)) + { + return false; + } + + totalInputSize = patchMetadata.DiffOldSize; + totalOutputSize = patchMetadata.DiffNewSize; + return true; + } + } + + /// + /// Try retrieves both total Input and Output size from a path of the patch file. + /// + /// The path of the patch file. + /// The total size of an Input File/Directory. + /// The total size of an Output File/Directory. + /// + /// Returns if both and are successfully retrieved. + /// Otherwise, if the patch context or the file is invalid or corrupted. + /// + public static bool TryGetDiffSizeInfo( + string patchFilePath, + out long totalInputSize, + out long totalOutputSize) + { + HDiffInfo info = CreateInstance(CreateStream); + try + { + return info.TryGetDiffSizeInfo(out totalInputSize, + out totalOutputSize); + } + finally + { + info.Dispose(); + } + + (Stream, bool) CreateStream(long pos) + { + FileStream fileStream = File.Open(patchFilePath, FileMode.Open, FileAccess.Read, FileShare.Read); + fileStream.Position = pos; + return (fileStream, false); + } + } + + /// + /// Try retrieves both total Input and Output size from a of the patch data. + /// + /// A factory delegate which creates the instance of the patch file. + /// The total size of an Input File/Directory. + /// The total size of an Output File/Directory. + /// + /// Returns if both and are successfully retrieved. + /// Otherwise, if the patch context or the file stream is invalid or corrupted. + /// + public static bool TryGetDiffSizeInfo(CreateStream createStream, + out long totalInputSize, + out long totalOutputSize) + { + HDiffInfo info = CreateInstance(createStream); + try + { + return info.TryGetDiffSizeInfo(out totalInputSize, + out totalOutputSize); + } + finally + { + info.Dispose(); + } + } + + /// + /// Try retrieves both total Input and Output size from a of the patch data. + /// + /// A source instance of the patch data. + /// The total size of an Input File/Directory. + /// The total size of an Output File/Directory. + /// + /// Returns if both and are successfully retrieved. + /// Otherwise, if the patch context or the file stream is invalid or corrupted. + /// + public static bool TryGetDiffSizeInfo(Stream stream, + out long totalInputSize, + out long totalOutputSize) + { + return TryGetDiffSizeInfo(CreateStream, + out totalInputSize, + out totalOutputSize); + + (Stream, bool) CreateStream(long pos) + { + stream.Position = pos; + return (stream, true); + } + } + + /// + /// Try gets the from an instance. + /// + /// The type of the content struct + /// The unmanaged array to get the from. + /// + /// Returns a non-empty if the array is valid. + /// Otherwise, returns an empty if the array is invalid. + /// + public static unsafe Span TryGetUnmanagedArraySpan(UnmanagedArray* array) + where T : unmanaged + => array == null || + (MetadataExtension.TryGetMetadataType(array, out MetadataTypeConst metadataType) && metadataType != MetadataTypeConst.UnmanagedArrayType) + ? Span.Empty : array->GetSpan(); + + /// + /// Try gets the from an instance. + /// + /// The type of the content struct + /// The unmanaged array to get the from. + /// + /// Returns a non-empty if the array is valid. + /// Otherwise, returns an empty if the array is invalid. + /// + public static unsafe Span TryGetUnmanagedArraySpan(this ref UnmanagedArray array) + where T : unmanaged + => TryGetUnmanagedArraySpan((UnmanagedArray*)Unsafe.AsPointer(ref array)); +} \ No newline at end of file diff --git a/SharpHPatchZ/HPatch.cs b/SharpHPatchZ/HPatch.cs new file mode 100644 index 0000000..eedd445 --- /dev/null +++ b/SharpHPatchZ/HPatch.cs @@ -0,0 +1,324 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpHPatchZ.Extension; +using SharpHPatchZ.Header; +using SharpHPatchZ.IO.Reader; +using SharpHPatchZ.Patch; + +namespace SharpHPatchZ; + +/// +/// A factory delegate which creates an instance of a of the patch from specified . +/// +/// The offset position of the to begin from. +/// +/// Returns both as Stream property and as LeaveOpen property. +/// Return LeaveOpen == if you want to keep the source opened.
+/// Otherwise, set LeaveOpen == to dispose the returned instance after use. +///
+public delegate (Stream Stream, bool LeaveOpen) CreateStream(long position); + +/// +/// A factory delegate which creates an instance of a of the patch from specified asynchronously. +/// +/// The offset position of the to begin from. +/// A cancellation token for the cancellation event while async operation is happening. +/// +/// Returns both as Stream property and as LeaveOpen property. +/// Return LeaveOpen == if you want to keep the source opened.
+/// Otherwise, set LeaveOpen == to dispose the returned instance after use. +///
+public delegate ValueTask<(Stream Stream, bool LeaveOpen)> CreateStreamAsync(long position, CancellationToken token); + +/// +/// A static class containing main functionality of the SharpHPatchZ library. +/// +public static partial class HPatch +{ + /// + /// Creates a context for the patch processing. + /// + /// A factory delegate which creates a instance of the patch file. + /// Options during the initialization process. For more usage information, see . + /// Returns a context and information struct about the patch file. + public static HDiffInfo CreateInstance(CreateStream createPatchStream, + InitializeOptions initializeOptions = default) + { + try + { + (Stream stream, bool leaveOpen) = createPatchStream(0); + using BittableStreamReader reader = new(stream, leaveOpen: leaveOpen); + + string signature = reader.ReadStringToNull(); + HDiffInfo info = default; + HeaderReader.ReadHeaderSignature(signature, ref info); + HeaderReader.ReadHDiffHeaderMetadata(ref info, reader, initializeOptions); + + return info; + } + catch (EndOfStreamException eofStream) + { + throw ExceptionHelper.ThrowHDiffEndOfFileOrData(eofStream); + } + } + + /// + /// Creates a context for the patch processing. + /// + /// A path to the patch file. + /// Options during the initialization process. For more usage information, see . + /// Returns a context and information struct about the patch file. + public static HDiffInfo CreateInstance(string patchPath, + InitializeOptions initializeOptions = default) + { + try + { + (Stream stream, bool leaveOpen) = CreateFileStreamWrapper(patchPath, 0); + using BittableStreamReader reader = new(stream, leaveOpen: leaveOpen); + + string signature = reader.ReadStringToNull(); + HDiffInfo info = default; + HeaderReader.ReadHeaderSignature(signature, ref info); + HeaderReader.ReadHDiffHeaderMetadata(ref info, reader, initializeOptions); + + return info; + } + catch (EndOfStreamException eofStream) + { + throw ExceptionHelper.ThrowHDiffEndOfFileOrData(eofStream); + } + } + + /// + /// Creates a context for the patch processing asynchronously. + /// + /// A factory delegate creates a instance of the patch file from specified position/offset. + /// A cancellation token for the cancellation event while async operation is happening. + /// Returns a context and information struct about the patch file. + public static ValueTask CreateInstanceAsync( + CreateStreamAsync createPatchStreamAsync, + CancellationToken token = default) + => CreateInstanceAsync(createPatchStreamAsync, default, token); + + /// + /// Creates a context for the patch processing asynchronously. + /// + /// A path to the patch file. + /// A cancellation token for the cancellation event while async operation is happening. + /// Returns a context and information struct about the patch file. + public static ValueTask CreateInstanceAsync( + string patchPath, + CancellationToken token = default) + => CreateInstanceAsync(patchPath, default, token); + + /// + /// Creates a context for the patch processing asynchronously. + /// + /// A factory delegate creates a instance of the patch file from specified position/offset asynchronously. + /// Options during the initialization process. For more usage information, see . + /// A cancellation token for the cancellation event while async operation is happening. + /// Returns a context and information struct about the patch file. + public static async ValueTask CreateInstanceAsync( + CreateStreamAsync createPatchStreamAsync, + InitializeOptions initializeOptions, + CancellationToken token = default) + { + (Stream stream, bool leaveOpen) = await createPatchStreamAsync(0, token); +#if NET6_0_OR_GREATER + await +#endif + using BittableStreamReader reader = new(stream, leaveOpen: leaveOpen); + + string signature = await reader.ReadStringToNullAsync(token: token); + HDiffInfo info = default; + HeaderReader.ReadHeaderSignature(signature, ref info); + info = await HeaderReader.ReadHDiffHeaderMetadataAsync(info, reader, initializeOptions, token); + + return info; + } + + /// + /// Creates a context for the patch processing asynchronously. + /// + /// A path to the patch file. + /// Options during the initialization process. For more usage information, see . + /// A cancellation token for the cancellation event while async operation is happening. + /// Returns a context and information struct about the patch file. + public static async ValueTask CreateInstanceAsync( + string patchPath, + InitializeOptions initializeOptions, + CancellationToken token = default) + { + (Stream stream, bool leaveOpen) = await CreateFileStreamWrapperAsync(patchPath, 0); +#if NET6_0_OR_GREATER + await +#endif + using BittableStreamReader reader = new(stream, leaveOpen: leaveOpen); + + string signature = await reader.ReadStringToNullAsync(token: token); + HDiffInfo info = default; + HeaderReader.ReadHeaderSignature(signature, ref info); + info = await HeaderReader.ReadHDiffHeaderMetadataAsync(info, reader, initializeOptions, token); + + return info; + } + + /// + /// Performs patch routines to the specified Input and Output paths. + /// + /// The struct containing context and information about the patch file. + /// A factory delegate which creates a instance of the patch file. + /// The specified Input path of a file or directory. + /// The specified Output path of a file or directory to be written to + /// Options during the patching process. For more usage information, see . + /// A struct containing the specified delegated method to be used to report the progress of the patch process. Use to create the struct and pass the callback. + /// A cancellation token for the cancellation event while patching operation is happening. + /// + /// Returns a result of the patching process. The returned result can be implicitly cast into a nullable or .
+ /// If cast into a , the means that the patching process has been successful. Otherwise, failed and would be not .
+ /// If cast into a nullable and the result is , meaning that the patching process has been successful. Otherwise, failed. + ///
+ public static PatchResult Patch(HDiffInfo info, + CreateStream createPatchStream, + string inputPath, + string outputPath, + PatchOptions options = default, + ProgressCallback progressCallback = default, + CancellationToken token = default) + { + try + { + using PatcherBase patcher = PatcherFactory.CreateFromInfo(ref info, createPatchStream, options, progressCallback); + patcher.StartPatch(inputPath, outputPath, token); + return true; + } + catch (Exception ex) + { + return ex; + } + } + + + + /// + /// Performs patch routines to the specified Input and Output paths. + /// + /// The struct containing context and information about the patch file. + /// The specified Patch file path. + /// The specified Input path of a file or directory. + /// The specified Output path of a file or directory to be written to + /// Options during the patching process. For more usage information, see . + /// A struct containing the specified delegated method to be used to report the progress of the patch process. Use to create the struct and pass the callback. + /// A cancellation token for the cancellation event while patching operation is happening. + /// + /// Returns a result of the patching process. The returned result can be implicitly cast into a nullable or .
+ /// If cast into a , the means that the patching process has been successful. Otherwise, failed and would be not .
+ /// If cast into a nullable and the result is , meaning that the patching process has been successful. Otherwise, failed. + ///
+ public static PatchResult Patch(HDiffInfo info, + string patchPath, + string inputPath, + string outputPath, + PatchOptions options = default, + ProgressCallback progressCallback = default, + CancellationToken token = default) + { + try + { + using PatcherBase patcher = PatcherFactory.CreateFromInfo(ref info, + pos => CreateFileStreamWrapper(patchPath, pos), + options, + progressCallback); + patcher.StartPatch(inputPath, outputPath, token); + return true; + } + catch (Exception ex) + { + return ex; + } + } + + /// + /// Performs patch routines to the specified Input and Output paths asynchronously + /// + /// The struct containing context and information about the patch file. + /// A factory delegate which creates a instance of the patch file asynchronously. + /// The specified Input path of a file or directory. + /// The specified Output path of a file or directory to be written to + /// Options during the patching process. For more usage information, see . + /// A struct containing the specified delegated method to be used to report the progress of the patch process. Use to create the struct and pass the callback. + /// A cancellation token for the cancellation event while patching operation is happening. + /// + /// Returns a result of the patching process. The returned result can be implicitly cast into a nullable or .
+ /// If cast into a , the means that the patching process has been successful. Otherwise, failed and would be not .
+ /// If cast into a nullable and the result is , meaning that the patching process has been successful. Otherwise, failed. + ///
+ public static async Task PatchAsync( + HDiffInfo info, + CreateStreamAsync createPatchStreamAsync, + string inputPath, + string outputPath, + PatchOptions options = default, + ProgressCallback progressCallback = default, + CancellationToken token = default) + { + try + { +#if NET6_0_OR_GREATER + await +#endif + using PatcherBase patcher = await PatcherFactory.CreateFromInfoAsync(info, createPatchStreamAsync, options, progressCallback, token); + await patcher.StartPatchAsync(inputPath, outputPath, token); + return true; + } + catch (Exception ex) + { + return ex; + } + } + + /// + /// Performs patch routines to the specified Input and Output paths asynchronously + /// + /// The struct containing context and information about the patch file. + /// The specified Patch file path. + /// The specified Input path of a file or directory. + /// The specified Output path of a file or directory to be written to + /// Options during the patching process. For more usage information, see . + /// A struct containing the specified delegated method to be used to report the progress of the patch process. Use to create the struct and pass the callback. + /// A cancellation token for the cancellation event while patching operation is happening. + /// + /// Returns a result of the patching process. The returned result can be implicitly cast into a nullable or .
+ /// If cast into a , the means that the patching process has been successful. Otherwise, failed and would be not .
+ /// If cast into a nullable and the result is , meaning that the patching process has been successful. Otherwise, failed. + ///
+ public static async Task PatchAsync( + HDiffInfo info, + string patchPath, + string inputPath, + string outputPath, + PatchOptions options = default, + ProgressCallback progressCallback = default, + CancellationToken token = default) + { + try + { +#if NET6_0_OR_GREATER + await +#endif + using PatcherBase patcher = await PatcherFactory.CreateFromInfoAsync(info, + (pos, _) => CreateFileStreamWrapperAsync(patchPath, pos), + options, + progressCallback, + token); + await patcher.StartPatchAsync(inputPath, outputPath, token); + return true; + } + catch (Exception ex) + { + return ex; + } + } +} diff --git a/SharpHPatchZ/Header/HDiffChecksum.cs b/SharpHPatchZ/Header/HDiffChecksum.cs new file mode 100644 index 0000000..47faca2 --- /dev/null +++ b/SharpHPatchZ/Header/HDiffChecksum.cs @@ -0,0 +1,14 @@ +namespace SharpHPatchZ.Header; + +/// +/// Determines the type of the Checksum used within the HDiff file. +/// +public enum HDiffChecksum +{ + /// The patch does not include checksums. + NoChecksum, + /// The patch uses the fast Adler-64 checksum. + FAdler64, + /// The patch uses the CRC-32 checksum. + Crc32 +} diff --git a/SharpHPatchZ/Header/HDiffChecksumResult.cs b/SharpHPatchZ/Header/HDiffChecksumResult.cs new file mode 100644 index 0000000..74da483 --- /dev/null +++ b/SharpHPatchZ/Header/HDiffChecksumResult.cs @@ -0,0 +1,24 @@ +using System.Runtime.InteropServices; + +namespace SharpHPatchZ.Header; + +/// +/// Describes the result of validating a patch checksum. +/// +[StructLayout(LayoutKind.Sequential)] +public struct HDiffChecksumResult +{ + /// Indicates whether the patch's checksum algorithm is supported. + public bool HasChecksumSupport; + /// Indicates whether checksum validation succeeded. + public bool IsSuccessful; + /// Contains the parsed associated with the validation. + public HDiffInfo DiffInfo; + + public static implicit operator bool(HDiffChecksumResult result) + { + return !result.HasChecksumSupport || + // Pass it as true anyways while Diff has no checksum support + result.IsSuccessful; + } +} diff --git a/SharpHPatchZ/Header/HDiffCompression.cs b/SharpHPatchZ/Header/HDiffCompression.cs new file mode 100644 index 0000000..a87ef9a --- /dev/null +++ b/SharpHPatchZ/Header/HDiffCompression.cs @@ -0,0 +1,25 @@ +// ReSharper disable UnusedMember.Global +// ReSharper disable IdentifierTypo +// ReSharper disable InconsistentNaming +namespace SharpHPatchZ.Header; + +/// +/// Determines the Compression Type of the HDiff file. +/// +public enum HDiffCompression +{ + /// The patch data is not compressed. + Uncompressed, + /// The patch data uses LZMA compression. + Lzma, + /// The patch data uses LZMA2 compression. + Lzma2, + /// The patch data uses Zlib compression. + Zlib, + /// The patch data uses parallel BZip2 compression. + PBZ2, + /// The patch data uses BZip2 compression. + BZ2, + /// The patch data uses Zstandard compression. + Zstd +} diff --git a/SharpHPatchZ/Header/HDiffInfo.cs b/SharpHPatchZ/Header/HDiffInfo.cs new file mode 100644 index 0000000..ffc7ed8 --- /dev/null +++ b/SharpHPatchZ/Header/HDiffInfo.cs @@ -0,0 +1,77 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SharpHPatchZ.Extension; +using SharpHPatchZ.Header.Metadata; + +namespace SharpHPatchZ.Header; + +/// +/// Holds the parsed header, initialization options, and unmanaged metadata for an HDiff patch. +/// +[StructLayout(LayoutKind.Sequential)] +public unsafe struct HDiffInfo : IDisposable +{ + /// Identifies the patch format. + public HDiffMagic MagicType; + /// Identifies the compression algorithm used by the patch. + public HDiffCompression CompressionType; + /// Identifies the checksum algorithm used by the patch. + public HDiffChecksum ChecksumType; + /// Contains the used to initialize this . + public InitializeOptions InitializeOptions; + /// Points to format-specific unmanaged metadata owned by this instance. + public void* MetadataP; + + internal ref T MetadataAs() + where T : unmanaged, IMetadataInit + => ref MetadataP == null ? + ref Unsafe.NullRef() : + ref Unsafe.AsRef(MetadataP); + + internal ref T AllocMetadata() + where T : unmanaged, IMetadataInit + { + if (MetadataP != null) MemoryAlloc.Free(MetadataP); + + MetadataP = MemoryAlloc.Alloc(1, true); + ((T*)MetadataP)->Init(); // Start metadata initialization + return ref Unsafe.AsRef(MetadataP); + } + + internal ref PatchMetadata GetPatchMetadata() + { + void* patchMetadataP; + if (MetadataExtension.TryGetMetadataType(MetadataP, out MetadataTypeConst rootMetadataType) && + rootMetadataType == MetadataTypeConst.DirectoryPatchMetadataType) + { + ref DirectoryPatchMetadata dirPatchMetadata = ref MetadataAs(); + if (Unsafe.IsNullRef(ref dirPatchMetadata) || dirPatchMetadata.PatchMetadataP == null) + { + throw ExceptionHelper.ThrowHDiffInfoPatchMetadataNotAllocated(); + } + + patchMetadataP = dirPatchMetadata.PatchMetadataP; + } + else + { + patchMetadataP = MetadataP; + } + + if (patchMetadataP == null) + { + throw ExceptionHelper.ThrowHDiffInfoPatchMetadataNotAllocated(); + } + + return ref Unsafe.AsRef(patchMetadataP); + } + + /// Releases the unmanaged metadata owned by this instance. + public void Dispose() + { + if (MetadataP == null) return; + + MemoryAlloc.Free(MetadataP); + MetadataP = null; + } +} diff --git a/SharpHPatchZ/Header/HDiffMagic.cs b/SharpHPatchZ/Header/HDiffMagic.cs new file mode 100644 index 0000000..82c70aa --- /dev/null +++ b/SharpHPatchZ/Header/HDiffMagic.cs @@ -0,0 +1,22 @@ +namespace SharpHPatchZ.Header; + +/// +/// The signature type of the HDiff patch format. +/// +public enum HDiffMagic +{ + /// + /// Whether to indicate that the file is not an HDiff Patch or unsupported format. + /// + Unknown, + + /// + /// Patch is a single HDiff format (HDiff13). + /// + HDiff13, + + /// + /// Patch is a Directory HDiff extension for HDiff13 format (HDiff19+HDiff13 aka DirHDiff). + /// + HDiff19 +} diff --git a/SharpHPatchZ/Header/HeaderReader.cs b/SharpHPatchZ/Header/HeaderReader.cs new file mode 100644 index 0000000..faef2ce --- /dev/null +++ b/SharpHPatchZ/Header/HeaderReader.cs @@ -0,0 +1,473 @@ +using System; +using System.Buffers; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using SharpHPatchZ.Extension; +using SharpHPatchZ.Header.Metadata; +using SharpHPatchZ.IO.Compression; +using SharpHPatchZ.IO.Reader; + +namespace SharpHPatchZ.Header; + +/// +/// Provides operations for parsing HDiff patch headers. +/// +public class HeaderReader +{ + private delegate ref PatchMetadata PatchMetadataAllocator(ref HDiffInfo info); + + /// Parses a patch header signature into an existing value. + /// The header signature to parse. + /// The patch information to populate. + public static void ReadHeaderSignature(ReadOnlySpan signature, ref HDiffInfo info) + { + ReadBasicHeaderSignature(signature, + out info.MagicType, + out info.CompressionType, + out info.ChecksumType); + } + + internal static void ReadBasicHeaderSignature( + ReadOnlySpan signature, + out HDiffMagic magicType, + out HDiffCompression compressionType, + out HDiffChecksum checksumType) + { + Unsafe.SkipInit(out magicType); + Unsafe.SkipInit(out compressionType); + Unsafe.SkipInit(out checksumType); + + Span ranges = stackalloc Range[4]; + int signatureSplits = signature.GetSplits(ranges, '&'); + + if (signature.IsEmpty || + signatureSplits == 0) + { + throw ExceptionHelper.ThrowHDiffHeaderSignatureEmptyOrUnreadable(); + } + +#if !NET6_0_OR_GREATER + string magicSpan = signature[ranges[0]].ToString(); + string compressionTypeSpan = signature[ranges[1]].ToString(); + string checksumTypeSpan = signature[ranges[2]].ToString(); +#else + ReadOnlySpan magicSpan = signature[ranges[0]]; + ReadOnlySpan compressionTypeSpan = signature[ranges[1]]; + ReadOnlySpan checksumTypeSpan = signature[ranges[2]]; +#endif + + if (!Enum.TryParse(magicSpan, true, out magicType)) + { + throw ExceptionHelper.ThrowHDiffHeaderMagicNotSupported(magicSpan); + } + + switch (magicType) + { + // Parse HDIFF19 (Directory Patch) enums + case HDiffMagic.HDiff19: + { + if (compressionTypeSpan.Length != 0 && + !Enum.TryParse(compressionTypeSpan, true, out compressionType)) + { + throw ExceptionHelper.ThrowHDiffHeaderCompressionNotSupported(compressionTypeSpan); + } + + if (checksumTypeSpan.Length != 0 && + !Enum.TryParse(checksumTypeSpan, true, out checksumType)) + { + throw ExceptionHelper.ThrowHDiffHeaderChecksumNotSupported(checksumTypeSpan); + } + + return; + } + // Parse HDIFF13 (Single Patch) enums + case HDiffMagic.HDiff13: + { + if (compressionTypeSpan.Length != 0 && + !Enum.TryParse(compressionTypeSpan, true, out compressionType)) + { + throw ExceptionHelper.ThrowHDiffHeaderCompressionNotSupported(compressionTypeSpan); + } + + return; + } + case HDiffMagic.Unknown: + default: + throw ExceptionHelper.ThrowHDiffHeaderMagicNotSupported(magicSpan); + } + } + + internal static void ReadHDiffHeaderMetadata( + ref HDiffInfo info, + BittableStreamReader streamReader, + InitializeOptions initializeOptions) + { + info.InitializeOptions = initializeOptions; + PatchMetadataAllocator patchMetadataAllocator = DefaultPatchMetadataAllocator; + switch (info.MagicType) + { + case HDiffMagic.HDiff19: + ReadHDiff19HeaderInfoCore(ref info, streamReader); + patchMetadataAllocator = HDiff19PatchMetadataAllocator; + // Continue reading HDiff13 data section. + goto case HDiffMagic.HDiff13; + case HDiffMagic.HDiff13: + ReadHDiff13HeaderInfoCore(ref info, streamReader, patchMetadataAllocator); + break; + } + } + + internal static async Task ReadHDiffHeaderMetadataAsync( + HDiffInfo info, + BittableStreamReader streamReader, + InitializeOptions initializeOptions, + CancellationToken token) + { + info.InitializeOptions = initializeOptions; + PatchMetadataAllocator patchMetadataAllocator = DefaultPatchMetadataAllocator; + switch (info.MagicType) + { + case HDiffMagic.HDiff19: + info = await ReadHDiff19HeaderInfoAsyncCore(info, + streamReader, + token); + + patchMetadataAllocator = HDiff19PatchMetadataAllocator; + // Continue reading HDiff13 data section. + goto case HDiffMagic.HDiff13; + case HDiffMagic.HDiff13: + info = await ReadHDiff13HeaderInfoAsyncCore(info, + streamReader, + patchMetadataAllocator, + token); + break; + } + + return info; + } + + private static ref PatchMetadata DefaultPatchMetadataAllocator(ref HDiffInfo info) + => ref info.AllocMetadata(); + + private static unsafe ref PatchMetadata HDiff19PatchMetadataAllocator(ref HDiffInfo info) + { + PatchMetadata* patchMetadata = MemoryAlloc.Alloc(); + patchMetadata->Init(); + + ref DirectoryPatchMetadata dirPatchMetadata = ref info.MetadataAs(); + dirPatchMetadata.PatchMetadataP = patchMetadata; + return ref Unsafe.AsRef(patchMetadata); + } + + private static unsafe void ReadHDiff19HeaderInfoCore( + ref HDiffInfo info, + BittableStreamReader streamReader) + { + byte isInputDir = streamReader.ReadByte(); + byte isOutputDir = streamReader.ReadByte(); + + int inputPathEntryCount = (int)streamReader.ReadLong7Bit(); + long inputPathEntryBufferSize = streamReader.ReadLong7Bit(); + int outputPathEntryCount = (int)streamReader.ReadLong7Bit(); + long outputPathEntryBufferSize = streamReader.ReadLong7Bit(); + + int inputRefFileCount = (int)streamReader.ReadLong7Bit(); + long inputPathEntryTotalSize = streamReader.ReadLong7Bit(); + int outputRefFileCount = (int)streamReader.ReadLong7Bit(); + long outputPathEntryTotalSize = streamReader.ReadLong7Bit(); + + int sameFilePathEntryCount = (int)streamReader.ReadLong7Bit(); + long sameFilePathEntryTotalSize = streamReader.ReadLong7Bit(); + + int newExecuteCount = streamReader.ReadInt7Bit(); + long privateReservedDataSize = streamReader.ReadLong7Bit(); + long privateExternDataSize = streamReader.ReadLong7Bit(); + long externDataSize = streamReader.ReadLong7Bit(); + + long headDataSize = streamReader.ReadLong7Bit(); + long headDataCompressedSize = streamReader.ReadLong7Bit(); + long checksumByteSize = streamReader.ReadLong7Bit(); + + int checksumDataLen = (int)checksumByteSize * 4; + Span checksumData = stackalloc byte[checksumDataLen]; + + streamReader.ReadBytes(checksumData); + + if (headDataCompressedSize > 0) + { + streamReader.ContinueWithDecompressor(new HDiffDecompressor(info.CompressionType), + headDataCompressedSize, + headDataSize); + } + + long headDataStartOffset = streamReader.Offset; + + UnmanagedArray* inputPathEntryArray = streamReader.CreateUnmanagedStringList(inputPathEntryCount, (int)inputPathEntryBufferSize); + UnmanagedArray* outputPathEntryArray = streamReader.CreateUnmanagedStringList(outputPathEntryCount, (int)outputPathEntryBufferSize); + UnmanagedArray* inputFilesIndexArray = streamReader.CreateUnmanagedInt64As32List(inputRefFileCount); + UnmanagedArray* outputFilesIndexArray = streamReader.CreateUnmanagedInt64As32List(outputRefFileCount); + + UnmanagedArray* inputFilesSizesArray = null; + if (info.InitializeOptions.IsKuroGamesHDiff) + inputFilesSizesArray = streamReader.CreateUnmanagedInt64List(inputRefFileCount); + + UnmanagedArray* outputFilesSizesArray = streamReader.CreateUnmanagedInt64List(outputRefFileCount); + + UnmanagedArray* outputFilesHashesArray = null; + if (info.InitializeOptions.IsKuroGamesHDiff) + outputFilesHashesArray = streamReader.CreateUnmanagedInt64List(outputRefFileCount); + + FileIndexPair* sameFilePathIndexPairArray = streamReader.CreateUnmanagedIndexPairList(sameFilePathEntryCount); + UnmanagedArray* newExecuteListArray = streamReader.CreateUnmanagedInt64As32List(newExecuteCount); + + if (streamReader.Offset - headDataStartOffset != headDataSize) + { + throw new InvalidDataException("The directory head data length does not match its declared length."); + } + + string sanityDiffPatchSignature = streamReader.ReadStringToNull(); + ReadBasicHeaderSignature(sanityDiffPatchSignature, out _, out _, out _); + + ref DirectoryPatchMetadata dirTypeMetadata = ref info.AllocMetadata(); + dirTypeMetadata.IsInputDir = isInputDir; + dirTypeMetadata.IsOutputDir = isOutputDir; + + EntryCountSizeInfo* inputPathCountSizeInfoP = dirTypeMetadata.InputPathCountSizeInfoP; + EntryCountSizeInfo* outputPathCountSizeInfoP = dirTypeMetadata.OutputPathCountSizeInfoP; + EntryCountSizeInfo* sameFilePathCountSizeInfoP = dirTypeMetadata.SameFilePathCountSizeInfoP; + ExternSizeInfo* externSizeInfo = dirTypeMetadata.ExternSizeInfoP; + ChunkSizeInfo* headDataSizeP = dirTypeMetadata.HeadDataSizeP; + ChecksumDataInfo* checksumDataInfoP = dirTypeMetadata.ChecksumDataInfoP; + + inputPathCountSizeInfoP->Count = inputPathEntryCount; + inputPathCountSizeInfoP->Size = inputPathEntryTotalSize; + outputPathCountSizeInfoP->Count = outputPathEntryCount; + outputPathCountSizeInfoP->Size = outputPathEntryTotalSize; + sameFilePathCountSizeInfoP->Count = sameFilePathEntryCount; + sameFilePathCountSizeInfoP->Size = sameFilePathEntryTotalSize; + + dirTypeMetadata.SameFilePathIndexPairP = sameFilePathIndexPairArray; + dirTypeMetadata.NewExecuteListP = newExecuteListArray; + + dirTypeMetadata.InputPathListP = inputPathEntryArray; + dirTypeMetadata.OutputPathListP = outputPathEntryArray; + dirTypeMetadata.InputFileIndexListP = inputFilesIndexArray; + dirTypeMetadata.InputFileSizeListP = inputFilesSizesArray; + dirTypeMetadata.OutputFileIndexListP = outputFilesIndexArray; + dirTypeMetadata.OutputFileSizeListP = outputFilesSizesArray; + dirTypeMetadata.OutputFileHashesListP = outputFilesHashesArray; + + externSizeInfo->NewExecuteCount = newExecuteCount; + externSizeInfo->PrivateReservedDataSize = privateReservedDataSize; + externSizeInfo->PrivateExternDataSize = privateExternDataSize; + externSizeInfo->ExternDataSize = externDataSize; + + headDataSizeP->Size = headDataSize; + headDataSizeP->CompressedSize = headDataCompressedSize; + + checksumDataInfoP->AllocBytes((int)checksumByteSize, 4); + checksumData.CopyTo(checksumDataInfoP->GetAllSpan()); + } + + private static async Task ReadHDiff19HeaderInfoAsyncCore( + HDiffInfo info, + BittableStreamReader streamReader, + CancellationToken token) + { + byte isInputDir = await streamReader.ReadByteAsync(token); + byte isOutputDir = await streamReader.ReadByteAsync(token); + + int inputPathEntryCount = (int)await streamReader.ReadLong7BitAsync(token); + long inputPathEntryBufferSize = await streamReader.ReadLong7BitAsync(token); + int outputPathEntryCount = (int)await streamReader.ReadLong7BitAsync(token); + long outputPathEntryBufferSize = await streamReader.ReadLong7BitAsync(token); + + int inputRefFileCount = (int)await streamReader.ReadLong7BitAsync(token); + long inputPathEntryTotalSize = await streamReader.ReadLong7BitAsync(token); + int outputRefFileCount = (int)await streamReader.ReadLong7BitAsync(token); + long outputPathEntryTotalSize = await streamReader.ReadLong7BitAsync(token); + + int sameFilePathEntryCount = (int)await streamReader.ReadLong7BitAsync(token); + long sameFilePathEntryTotalSize = await streamReader.ReadLong7BitAsync(token); + + int newExecuteCount = await streamReader.ReadInt7BitAsync(token); + long privateReservedDataSize = await streamReader.ReadLong7BitAsync(token); + long privateExternDataSize = await streamReader.ReadLong7BitAsync(token); + long externDataSize = await streamReader.ReadLong7BitAsync(token); + + long headDataSize = await streamReader.ReadLong7BitAsync(token); + long headDataCompressedSize = await streamReader.ReadLong7BitAsync(token); + long checksumByteSize = await streamReader.ReadLong7BitAsync(token); + + int checksumDataLen = (int)checksumByteSize * 4; + byte[] checksumData = ArrayPool.Shared.Rent(checksumDataLen); + await streamReader.ReadBytesAsync(checksumData.AsMemory(0, checksumDataLen), token); + + if (headDataCompressedSize > 0) + { + streamReader.ContinueWithDecompressor(new HDiffDecompressor(info.CompressionType), + headDataCompressedSize, + headDataSize); + } + + long headDataStartOffset = streamReader.Offset; + + nint inputPathEntryArray = await streamReader.CreateUnmanagedStringListAsync(inputPathEntryCount, (int)inputPathEntryBufferSize, token); + nint outputPathEntryArray = await streamReader.CreateUnmanagedStringListAsync(outputPathEntryCount, (int)outputPathEntryBufferSize, token); + nint inputFilesIndexArray = await streamReader.CreateUnmanagedInt64As32ListAsync(inputRefFileCount, token); + nint outputFilesIndexArray = await streamReader.CreateUnmanagedInt64As32ListAsync(outputRefFileCount, token); + + nint inputFilesSizesArray = 0; + if (info.InitializeOptions.IsKuroGamesHDiff) + inputFilesSizesArray = await streamReader.CreateUnmanagedInt64ListAsync(inputRefFileCount, token); + + nint outputFilesSizesArray = await streamReader.CreateUnmanagedInt64ListAsync(outputRefFileCount, token); + + nint outputFilesHashesArray = 0; + if (info.InitializeOptions.IsKuroGamesHDiff) + outputFilesHashesArray = await streamReader.CreateUnmanagedInt64ListAsync(outputRefFileCount, token); + + nint sameFilePathIndexPairArray = await streamReader.CreateUnmanagedIndexPairListAsync(sameFilePathEntryCount, token); + nint newExecuteListArray = await streamReader.CreateUnmanagedInt64As32ListAsync(newExecuteCount, token); + + if (streamReader.Offset - headDataStartOffset != headDataSize) + { + throw new InvalidDataException("The directory head data length does not match its declared length."); + } + + string sanityDiffPatchSignature = await streamReader.ReadStringToNullAsync(token); + ReadBasicHeaderSignature(sanityDiffPatchSignature, out _, out _, out _); + + try + { + ref DirectoryPatchMetadata dirTypeMetadata = ref info.AllocMetadata(); + dirTypeMetadata.IsInputDir = isInputDir; + dirTypeMetadata.IsOutputDir = isOutputDir; + + unsafe + { + EntryCountSizeInfo* inputPathCountSizeInfoP = dirTypeMetadata.InputPathCountSizeInfoP; + EntryCountSizeInfo* outputPathCountSizeInfoP = dirTypeMetadata.OutputPathCountSizeInfoP; + EntryCountSizeInfo* sameFilePathCountSizeInfoP = dirTypeMetadata.SameFilePathCountSizeInfoP; + ExternSizeInfo* externSizeInfo = dirTypeMetadata.ExternSizeInfoP; + ChunkSizeInfo* headDataSizeP = dirTypeMetadata.HeadDataSizeP; + ChecksumDataInfo* checksumDataInfoP = dirTypeMetadata.ChecksumDataInfoP; + + inputPathCountSizeInfoP->Count = inputPathEntryCount; + inputPathCountSizeInfoP->Size = inputPathEntryTotalSize; + outputPathCountSizeInfoP->Count = outputPathEntryCount; + outputPathCountSizeInfoP->Size = outputPathEntryTotalSize; + sameFilePathCountSizeInfoP->Count = sameFilePathEntryCount; + sameFilePathCountSizeInfoP->Size = sameFilePathEntryTotalSize; + + dirTypeMetadata.SameFilePathIndexPairP = (FileIndexPair*)sameFilePathIndexPairArray; + dirTypeMetadata.NewExecuteListP = (UnmanagedArray*)newExecuteListArray; + + dirTypeMetadata.InputPathListP = (UnmanagedArray*)inputPathEntryArray; + dirTypeMetadata.OutputPathListP = (UnmanagedArray*)outputPathEntryArray; + dirTypeMetadata.InputFileIndexListP = (UnmanagedArray*)inputFilesIndexArray; + dirTypeMetadata.InputFileSizeListP = (UnmanagedArray*)inputFilesSizesArray; + dirTypeMetadata.OutputFileIndexListP = (UnmanagedArray*)outputFilesIndexArray; + dirTypeMetadata.OutputFileSizeListP = (UnmanagedArray*)outputFilesSizesArray; + dirTypeMetadata.OutputFileHashesListP = (UnmanagedArray*)outputFilesHashesArray; + + externSizeInfo->NewExecuteCount = newExecuteCount; + externSizeInfo->PrivateReservedDataSize = privateReservedDataSize; + externSizeInfo->PrivateExternDataSize = privateExternDataSize; + externSizeInfo->ExternDataSize = externDataSize; + + headDataSizeP->Size = headDataSize; + headDataSizeP->CompressedSize = headDataCompressedSize; + + checksumDataInfoP->AllocBytes((int)checksumByteSize, 4); + checksumData.CopyTo(checksumDataInfoP->GetAllSpan()); + + return info; + } + } + finally + { + ArrayPool.Shared.Return(checksumData); + } + } + + private static unsafe void ReadHDiff13HeaderInfoCore( + ref HDiffInfo info, + BittableStreamReader streamReader, + PatchMetadataAllocator metadataAllocator) + { + long newSize = streamReader.ReadLong7Bit(); + long oldSize = streamReader.ReadLong7Bit(); + + int coverDataCount = (int)streamReader.ReadLong7Bit(); + + long coverDataSize = streamReader.ReadLong7Bit(); + long coverDataSizeC = streamReader.ReadLong7Bit(); + long rleControlDataSize = streamReader.ReadLong7Bit(); + long rleControlDataSizeC = streamReader.ReadLong7Bit(); + long rleCodeDataSize = streamReader.ReadLong7Bit(); + long rleCodeDataSizeC = streamReader.ReadLong7Bit(); + long newDiffSize = streamReader.ReadLong7Bit(); + long newDiffSizeC = streamReader.ReadLong7Bit(); + + ref PatchMetadata patchMetadata = ref metadataAllocator(ref info); + + patchMetadata.DiffNewSize = newSize; + patchMetadata.DiffOldSize = oldSize; + patchMetadata.CoverDataCount = coverDataCount; + + patchMetadata.CoverDataSizeP->Size = coverDataSize; + patchMetadata.CoverDataSizeP->CompressedSize = coverDataSizeC; + patchMetadata.RleControlDataSizeP->Size = rleControlDataSize; + patchMetadata.RleControlDataSizeP->CompressedSize = rleControlDataSizeC; + patchMetadata.RleCodeDataSizeP->Size = rleCodeDataSize; + patchMetadata.RleCodeDataSizeP->CompressedSize = rleCodeDataSizeC; + patchMetadata.NewDiffDataSizeP->Size = newDiffSize; + patchMetadata.NewDiffDataSizeP->CompressedSize = newDiffSizeC; + patchMetadata.DiffDataOffset = streamReader.OffsetUnderlyingStream; + } + + private static async Task ReadHDiff13HeaderInfoAsyncCore( + HDiffInfo info, + BittableStreamReader streamReader, + PatchMetadataAllocator metadataAllocator, + CancellationToken token) + { + long newSize = await streamReader.ReadLong7BitAsync(token); + long oldSize = await streamReader.ReadLong7BitAsync(token); + + int coverDataCount = (int)await streamReader.ReadLong7BitAsync(token); + + long coverDataSize = await streamReader.ReadLong7BitAsync(token); + long coverDataSizeC = await streamReader.ReadLong7BitAsync(token); + long rleControlDataSize = await streamReader.ReadLong7BitAsync(token); + long rleControlDataSizeC = await streamReader.ReadLong7BitAsync(token); + long rleCodeDataSize = await streamReader.ReadLong7BitAsync(token); + long rleCodeDataSizeC = await streamReader.ReadLong7BitAsync(token); + long newDiffSize = await streamReader.ReadLong7BitAsync(token); + long newDiffSizeC = await streamReader.ReadLong7BitAsync(token); + + unsafe + { + ref PatchMetadata patchMetadata = ref metadataAllocator(ref info); + + patchMetadata.DiffNewSize = newSize; + patchMetadata.DiffOldSize = oldSize; + patchMetadata.CoverDataCount = coverDataCount; + + patchMetadata.CoverDataSizeP->Size = coverDataSize; + patchMetadata.CoverDataSizeP->CompressedSize = coverDataSizeC; + patchMetadata.RleControlDataSizeP->Size = rleControlDataSize; + patchMetadata.RleControlDataSizeP->CompressedSize = rleControlDataSizeC; + patchMetadata.RleCodeDataSizeP->Size = rleCodeDataSize; + patchMetadata.RleCodeDataSizeP->CompressedSize = rleCodeDataSizeC; + patchMetadata.NewDiffDataSizeP->Size = newDiffSize; + patchMetadata.NewDiffDataSizeP->CompressedSize = newDiffSizeC; + patchMetadata.DiffDataOffset = streamReader.OffsetUnderlyingStream; + + return info; + } + } +} diff --git a/SharpHPatchZ/Header/Metadata/ChecksumDataInfo.cs b/SharpHPatchZ/Header/Metadata/ChecksumDataInfo.cs new file mode 100644 index 0000000..ec867da --- /dev/null +++ b/SharpHPatchZ/Header/Metadata/ChecksumDataInfo.cs @@ -0,0 +1,84 @@ +using System; +using System.Runtime.CompilerServices; +using SharpHPatchZ.Extension; + +namespace SharpHPatchZ.Header.Metadata; + +/// Owns the unmanaged checksum blocks stored in a directory-patch header. +public unsafe struct ChecksumDataInfo : IMetadataInit +{ + /// Initializes a new . + public ChecksumDataInfo() + { + Init(); + } + + /// + public void Init() + { + if (IsDisposed || IsInitialized) + { + return; + } + + IsInitialized = true; + IsDisposed = false; + MetadataType = MetadataTypeConst.ChecksumDataInfoType; + } + + /// + public void Dispose() + { + if (IsDisposed) + { + return; + } + + IsDisposed = true; + IsInitialized = false; + + if (_byte != null) MemoryAlloc.Free(_byte); + _byte = null; + } + + /// + public MetadataTypeConst MetadataType { get; private set; } + + /// + public bool IsInitialized + { + get => _isInitialized == 1; + private set => _isInitialized = value ? (byte)1 : (byte)0; + } + + /// + public bool IsDisposed + { + get => _isDisposed == 1; + private set => _isDisposed = value ? (byte)1 : (byte)0; + } + + private byte _isInitialized; + private byte _isDisposed; + + private int _dataSize; + private int _dataCount; + private void* _byte; + + /// Allocates zero-initialized unmanaged storage for checksum elements. + /// The size of each checksum element, in bytes. + /// The number of checksum elements. + public void AllocBytes(int dataSize, int elementCount) + => _byte = MemoryAlloc.Alloc((_dataSize = dataSize) * (_dataCount = elementCount), true); + + /// Gets a covering all allocated checksum bytes. + /// A over the complete checksum buffer. + public Span GetAllSpan() + => new(_byte, _dataCount * _dataSize); + + /// Gets the checksum element at the specified index. + /// The zero-based checksum element index. + /// A over the requested checksum element. + public Span GetSpan(int index) + => new(Unsafe.Add(_byte, index * _dataSize), _dataSize); +} diff --git a/SharpHPatchZ/Header/Metadata/ChunkSizeInfo.cs b/SharpHPatchZ/Header/Metadata/ChunkSizeInfo.cs new file mode 100644 index 0000000..739f21b --- /dev/null +++ b/SharpHPatchZ/Header/Metadata/ChunkSizeInfo.cs @@ -0,0 +1,13 @@ +using System.Runtime.InteropServices; + +namespace SharpHPatchZ.Header.Metadata; + +/// Stores the uncompressed and compressed sizes of a patch data chunk. +[StructLayout(LayoutKind.Sequential)] +public struct ChunkSizeInfo +{ + /// The uncompressed chunk size, in bytes. + public long Size; + /// The compressed chunk size, in bytes, or zero when the chunk is not compressed. + public long CompressedSize; +} diff --git a/SharpHPatchZ/Header/Metadata/DirectoryPatchMetadata.cs b/SharpHPatchZ/Header/Metadata/DirectoryPatchMetadata.cs new file mode 100644 index 0000000..09d5ca8 --- /dev/null +++ b/SharpHPatchZ/Header/Metadata/DirectoryPatchMetadata.cs @@ -0,0 +1,132 @@ +using System.Runtime.InteropServices; +using SharpHPatchZ.Extension; + +namespace SharpHPatchZ.Header.Metadata; + +/// Contains file lists, index mappings, and chunk information for a directory patch. +[StructLayout(LayoutKind.Sequential)] +public unsafe struct DirectoryPatchMetadata : IMetadataInit +{ + /// Initializes a new and its owned records. + public DirectoryPatchMetadata() + { + Init(); + } + + /// + public void Init() + { + if (IsDisposed || IsInitialized) + { + return; + } + + IsInitialized = true; + IsDisposed = false; + MetadataType = MetadataTypeConst.DirectoryPatchMetadataType; + InputPathCountSizeInfoP = MemoryAlloc.Alloc(1, true); + OutputPathCountSizeInfoP = MemoryAlloc.Alloc(1, true); + SameFilePathCountSizeInfoP = MemoryAlloc.Alloc(1, true); + ExternSizeInfoP = MemoryAlloc.Alloc(1, true); + HeadDataSizeP = MemoryAlloc.Alloc(1, true); + PatchMetadataP = MemoryAlloc.Alloc(1, true); + ChecksumDataInfoP = MemoryAlloc.Alloc(1, true); + } + + /// + public void Dispose() + { + if (IsDisposed || !IsInitialized) + { + return; + } + + IsDisposed = true; + IsInitialized = false; + MemoryAlloc.Free(InputPathCountSizeInfoP); + MemoryAlloc.Free(OutputPathCountSizeInfoP); + MemoryAlloc.Free(SameFilePathCountSizeInfoP); + MemoryAlloc.Free(SameFilePathIndexPairP); + + MemoryAlloc.Free(InputPathListP); + MemoryAlloc.Free(OutputPathListP); + MemoryAlloc.Free(InputFileIndexListP); + MemoryAlloc.Free(InputFileSizeListP); + MemoryAlloc.Free(OutputFileIndexListP); + MemoryAlloc.Free(OutputFileSizeListP); + MemoryAlloc.Free(OutputFileHashesListP); + + MemoryAlloc.Free(ExternSizeInfoP); + + MemoryAlloc.Free(HeadDataSizeP); + MemoryAlloc.Free(PatchMetadataP); + MemoryAlloc.Free(ChecksumDataInfoP); + MemoryAlloc.Free(NewExecuteListP); + } + + /// + public MetadataTypeConst MetadataType { get; private set; } + + /// + public bool IsInitialized + { + get => _isInitialized == 1; + private set => _isInitialized = value ? (byte)1 : (byte)0; + } + + /// + public bool IsDisposed + { + get => _isDisposed == 1; + private set => _isDisposed = value ? (byte)1 : (byte)0; + } + + private byte _isInitialized; + private byte _isDisposed; + + /// Indicates whether the patch input is a directory. + public byte IsInputDir; + /// Indicates whether the patch output is a directory. + public byte IsOutputDir; + + /// Points to input-path count and size information. + public EntryCountSizeInfo* InputPathCountSizeInfoP; + /// Points to output-path count and size information. + public EntryCountSizeInfo* OutputPathCountSizeInfoP; + /// Points to unchanged-path count and size information. + public EntryCountSizeInfo* SameFilePathCountSizeInfoP; + /// Points to mappings between unchanged input and output paths. + public FileIndexPair* SameFilePathIndexPairP; + + /// Points to the input path list. + public UnmanagedArray* InputPathListP; + /// Points to the output path list. + public UnmanagedArray* OutputPathListP; + /// Points to indexes of referenced input files. + public UnmanagedArray* InputFileIndexListP; + /// Points to sizes of referenced input files, when present. + public UnmanagedArray* InputFileSizeListP; + /// Points to indexes of referenced output files. + public UnmanagedArray* OutputFileIndexListP; + /// Points to sizes of referenced output files. + public UnmanagedArray* OutputFileSizeListP; + /// Points to hashes of referenced output files, when present. + public UnmanagedArray* OutputFileHashesListP; + + // Seems unused. + // TODO: See original code to see what it is. + /// Points to external-data size information. + public ExternSizeInfo* ExternSizeInfoP; + + // File Reference Chunk Info, including: + // - Filename string chunks + // - Checksum chunks + /// Points to directory header-data size information. + public ChunkSizeInfo* HeadDataSizeP; + /// Points to the embedded single-file patch metadata. + public PatchMetadata* PatchMetadataP; + /// Points to the directory-patch checksum data. + public ChecksumDataInfo* ChecksumDataInfoP; + /// Points to indexes of output entries that should be executable. + public UnmanagedArray* NewExecuteListP; +} diff --git a/SharpHPatchZ/Header/Metadata/EntryCountSizeInfo.cs b/SharpHPatchZ/Header/Metadata/EntryCountSizeInfo.cs new file mode 100644 index 0000000..2dc1b48 --- /dev/null +++ b/SharpHPatchZ/Header/Metadata/EntryCountSizeInfo.cs @@ -0,0 +1,13 @@ +using System.Runtime.InteropServices; + +namespace SharpHPatchZ.Header.Metadata; + +/// Stores the number and total byte size of a group of patch entries. +[StructLayout(LayoutKind.Sequential)] +public struct EntryCountSizeInfo +{ + /// The number of entries. + public int Count; + /// The total size of the entries, in bytes. + public long Size; +} diff --git a/SharpHPatchZ/Header/Metadata/ExternSizeInfo.cs b/SharpHPatchZ/Header/Metadata/ExternSizeInfo.cs new file mode 100644 index 0000000..91badf6 --- /dev/null +++ b/SharpHPatchZ/Header/Metadata/ExternSizeInfo.cs @@ -0,0 +1,17 @@ +using System.Runtime.InteropServices; + +namespace SharpHPatchZ.Header.Metadata; + +/// Stores size information for executable and external directory-patch data. +[StructLayout(LayoutKind.Sequential)] +public struct ExternSizeInfo +{ + /// The number of new executable entries. + public int NewExecuteCount; + /// The size of the private reserved data, in bytes. + public long PrivateReservedDataSize; + /// The size of the private external data, in bytes. + public long PrivateExternDataSize; + /// The size of the external data, in bytes. + public long ExternDataSize; +} diff --git a/SharpHPatchZ/Header/Metadata/FileIndexPair.cs b/SharpHPatchZ/Header/Metadata/FileIndexPair.cs new file mode 100644 index 0000000..8737523 --- /dev/null +++ b/SharpHPatchZ/Header/Metadata/FileIndexPair.cs @@ -0,0 +1,16 @@ +using System.Runtime.InteropServices; + +namespace SharpHPatchZ.Header.Metadata; + +/// Maps an input-file index to its corresponding output-file index. +[StructLayout(LayoutKind.Sequential)] +public struct FileIndexPair +{ + /// The input-file index. + public int OldIndex; + /// The output-file index. + public int NewIndex; + + /// + public override string ToString() => $"Old: {OldIndex} - New: {NewIndex}"; +} diff --git a/SharpHPatchZ/Header/Metadata/IMetadataInit.cs b/SharpHPatchZ/Header/Metadata/IMetadataInit.cs new file mode 100644 index 0000000..b68b595 --- /dev/null +++ b/SharpHPatchZ/Header/Metadata/IMetadataInit.cs @@ -0,0 +1,17 @@ +using System; + +namespace SharpHPatchZ.Header.Metadata; + +/// Defines lifecycle information and initialization for unmanaged patch metadata. +public interface IMetadataInit : IDisposable +{ + /// Gets the discriminator for this metadata type. + public MetadataTypeConst MetadataType { get; } + /// Gets whether the metadata has been initialized. + public bool IsInitialized { get; } + /// Gets whether the metadata has been disposed. + public bool IsDisposed { get; } + + /// Initializes the metadata and any resources it owns. + void Init(); +} diff --git a/SharpHPatchZ/Header/Metadata/MetadataTypeConst.cs b/SharpHPatchZ/Header/Metadata/MetadataTypeConst.cs new file mode 100644 index 0000000..7036837 --- /dev/null +++ b/SharpHPatchZ/Header/Metadata/MetadataTypeConst.cs @@ -0,0 +1,22 @@ +using System; + +namespace SharpHPatchZ.Header.Metadata; + +/// Identifies the concrete kind of unmanaged patch metadata. +[Flags] +public enum MetadataTypeConst : short +{ + /// Mask applied to values that identify metadata. + IsMetadataType = unchecked((short)0b_10000000_10000000), + + /// Identifies . + PatchMetadataType = IsMetadataType | 0b_01000000_00000000, + /// Identifies . + DirectoryPatchMetadataType = IsMetadataType | 0b_00100000_00000000, + /// Identifies . + ChecksumDataInfoType = IsMetadataType | 0b_00010000_00000000, + /// Identifies . + UnmanagedArrayType = IsMetadataType | 0b_00001000_00000000, + /// Identifies . + Utf16UnmanagedStringType = IsMetadataType | 0b_00000100_00000000 +} diff --git a/SharpHPatchZ/Header/Metadata/PatchMetadata.cs b/SharpHPatchZ/Header/Metadata/PatchMetadata.cs new file mode 100644 index 0000000..c72e025 --- /dev/null +++ b/SharpHPatchZ/Header/Metadata/PatchMetadata.cs @@ -0,0 +1,91 @@ +using System.Runtime.InteropServices; +using SharpHPatchZ.Extension; + +namespace SharpHPatchZ.Header.Metadata; + +/// Contains the size and offset metadata required to apply a single-file patch. +[StructLayout(LayoutKind.Sequential)] +public unsafe struct PatchMetadata : IMetadataInit +{ + /// Initializes a new and its records. + public PatchMetadata() + { + Init(); + } + + /// + public void Init() + { + if (IsDisposed || IsInitialized) + { + return; + } + + IsInitialized = true; + IsDisposed = false; + MetadataType = MetadataTypeConst.PatchMetadataType; + CoverDataSizeP = MemoryAlloc.Alloc(1, true); + RleControlDataSizeP = MemoryAlloc.Alloc(1, true); + RleCodeDataSizeP = MemoryAlloc.Alloc(1, true); + NewDiffDataSizeP = MemoryAlloc.Alloc(1, true); + } + + /// + public void Dispose() + { + if (IsDisposed || !IsInitialized) + { + return; + } + + IsDisposed = true; + IsInitialized = false; + MemoryAlloc.Free(CoverDataSizeP); + MemoryAlloc.Free(RleControlDataSizeP); + MemoryAlloc.Free(RleCodeDataSizeP); + MemoryAlloc.Free(NewDiffDataSizeP); + CoverDataSizeP = null; + RleControlDataSizeP = null; + RleCodeDataSizeP = null; + NewDiffDataSizeP = null; + } + + /// + public MetadataTypeConst MetadataType { get; private set; } + + /// + public bool IsInitialized + { + get => _isInitialized == 1; + private set => _isInitialized = value ? (byte)1 : (byte)0; + } + + /// + public bool IsDisposed + { + get => _isDisposed == 1; + private set => _isDisposed = value ? (byte)1 : (byte)0; + } + + private byte _isInitialized; + private byte _isDisposed; + + /// The expected output size, in bytes. + public long DiffNewSize; + /// The expected input size, in bytes. + public long DiffOldSize; + /// The byte offset at which patch data begins. + public long DiffDataOffset; + + /// The number of cover-data entries in the patch. + public int CoverDataCount; + + /// Points to the cover-data size record. + public ChunkSizeInfo* CoverDataSizeP; + /// Points to the RLE control-data size record. + public ChunkSizeInfo* RleControlDataSizeP; + /// Points to the RLE code-data size record. + public ChunkSizeInfo* RleCodeDataSizeP; + /// Points to the new-difference-data size record. + public ChunkSizeInfo* NewDiffDataSizeP; +} diff --git a/SharpHPatchZ/Header/Metadata/UnmanagedArray.cs b/SharpHPatchZ/Header/Metadata/UnmanagedArray.cs new file mode 100644 index 0000000..2927810 --- /dev/null +++ b/SharpHPatchZ/Header/Metadata/UnmanagedArray.cs @@ -0,0 +1,138 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SharpHPatchZ.Extension; + +namespace SharpHPatchZ.Header.Metadata; + +/// Owns a contiguous unmanaged array of values. +/// The unmanaged element type. +[StructLayout(LayoutKind.Sequential)] +public unsafe struct UnmanagedArray : IMetadataInit + where T : unmanaged +{ + /// Initializes a new empty . + public UnmanagedArray() + { + Init(); + } + + /// + public void Init() + { + if (IsDisposed || IsInitialized) + { + return; + } + + TypeSize = sizeof(T); + IsInitialized = true; + IsDisposed = false; + MetadataType = MetadataTypeConst.UnmanagedArrayType; + } + + /// + public void Dispose() + { + if (IsDisposed || !IsInitialized || Data == null) + { + return; + } + + IsDisposed = true; + IsInitialized = false; + T* oldData = Data; + Data = null; + + if (oldData != null) + { + // Try to automatically dispose if data is a member of IMetadataInit + ref byte startRef = ref Unsafe.AsRef(oldData); + ref byte endRef = ref Unsafe.Add(ref startRef, TypeSize * Length); + if (startRef.TryGetMetadataType(out _)) + { + while (Unsafe.IsAddressLessThan(ref startRef, ref endRef)) + { + startRef.TryDisposeIfMetadataType(); + startRef = ref Unsafe.Add(ref startRef, TypeSize); + } + } + } + + MemoryAlloc.Free(oldData); + } + + /// + public MetadataTypeConst MetadataType { get; private set; } + + /// + public bool IsInitialized + { + get => _isInitialized == 1; + private set => _isInitialized = value ? (byte)1 : (byte)0; + } + + /// + public bool IsDisposed + { + get => _isDisposed == 1; + private set => _isDisposed = value ? (byte)1 : (byte)0; + } + + private byte _isInitialized; + private byte _isDisposed; + + /// The number of elements in the array. + public int Length; + /// The size of each element, in bytes. + public int TypeSize; + /// Points to the first element in the unmanaged allocation. + public T* Data; + + internal Span GetSpan() => Data == null ? Span.Empty : new Span(Data, Length); + + /// Allocates an unmanaged array descriptor and its element storage. + /// The number of elements to allocate. + /// Whether to zero-initialize the element storage. + /// A pointer to the allocated array descriptor. + public static UnmanagedArray* CreateAllocUnsafe(int count, bool initialize = false) + { + UnmanagedArray* alloc = MemoryAlloc.Alloc>(); + alloc->Data = MemoryAlloc.Alloc(count, initialize); + alloc->Length = count; + alloc->Init(); + + return alloc; + } + + /// Creates an array value backed by newly allocated unmanaged storage. + /// The number of elements to allocate. + /// Whether to zero-initialize the element storage. + /// The initialized . + public static UnmanagedArray CreateAlloc(int count, bool initialize = false) + { + var array = new UnmanagedArray + { + Data = MemoryAlloc.Alloc(count, initialize), + Length = count + }; + + array.Init(); + return array; + } + + public static implicit operator Span(UnmanagedArray unmanagedSpan) => unmanagedSpan.GetSpan(); + + /// Gets a reference to the element at the specified index. + /// The zero-based element index. + /// A reference to the requested element. + /// is outside the array bounds. + public ref T this[int index] + { + get + { + if (index > Length - 1) throw new ArgumentOutOfRangeException(); + return ref Unsafe.AsRef(Unsafe.Add(Data, index)); + } + } +} diff --git a/SharpHPatchZ/Header/Metadata/Utf16UnmanagedString.cs b/SharpHPatchZ/Header/Metadata/Utf16UnmanagedString.cs new file mode 100644 index 0000000..3b6cb7d --- /dev/null +++ b/SharpHPatchZ/Header/Metadata/Utf16UnmanagedString.cs @@ -0,0 +1,181 @@ +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using SharpHPatchZ.Extension; + +namespace SharpHPatchZ.Header.Metadata; + +/// Owns a UTF-16 string stored in unmanaged memory. +[StructLayout(LayoutKind.Sequential)] +public unsafe struct Utf16UnmanagedString : IMetadataInit +{ + /// Initializes a new empty . + public Utf16UnmanagedString() + { + Init(); + } + + /// + public void Init() + { + if (IsDisposed || IsInitialized) + { + return; + } + + IsInitialized = true; + IsDisposed = false; + MetadataType = MetadataTypeConst.Utf16UnmanagedStringType; + } + + /// + public void Dispose() + { + if (IsDisposed || !IsInitialized || Native is { Chars: 0, Length: 0 }) + { + return; + } + + IsDisposed = true; + IsInitialized = false; + NativeStringW old = Native; + Native = NativeStringW.Empty; + + MemoryAlloc.Free((char*)old.Chars); + } + + /// + public MetadataTypeConst MetadataType { get; private set; } + + /// + public bool IsInitialized + { + get => _isInitialized == 1; + private set => _isInitialized = value ? (byte)1 : (byte)0; + } + + /// + public bool IsDisposed + { + get => _isDisposed == 1; + private set => _isDisposed = value ? (byte)1 : (byte)0; + } + + private byte _isInitialized; + private byte _isDisposed; + /// The native string pointer and length. + public NativeStringW Native; + + /// Creates an unmanaged UTF-16 string from UTF-8 bytes. + /// The UTF-8 bytes to convert. + /// A newly allocated . +#if NET6_0_OR_GREATER + [SkipLocalsInit] +#endif + public static Utf16UnmanagedString CreateFromManaged(ReadOnlySpan source) + { + int maxLenTransform = Encoding.Unicode.GetMaxByteCount(source.Length); + char[]? tempCharsBuffer = maxLenTransform <= 1024 + ? null + : ArrayPool.Shared.Rent(maxLenTransform); + + Span tempCharsSpan = tempCharsBuffer ?? stackalloc char[maxLenTransform]; + try + { + return TransformUtf8ToUnicode(source, tempCharsSpan); + } + finally + { + if (tempCharsBuffer != null) ArrayPool.Shared.Return(tempCharsBuffer); + } + } + + /// Creates an unmanaged UTF-16 string from managed characters. + /// The characters to copy. + /// A newly allocated . +#if NET6_0_OR_GREATER + [SkipLocalsInit] +#endif + public static Utf16UnmanagedString CreateFromManaged(scoped ReadOnlySpan source) + { + int lengthToAlloc = source.Length + 1; + char* nativeChar = MemoryAlloc.Alloc(lengthToAlloc, true); + + source.CopyTo(new Span(nativeChar, source.Length)); + Utf16UnmanagedString thisStruct = new() + { + Native = new NativeStringW(nativeChar, source.Length) + }; + thisStruct.Init(); + + return thisStruct; + } + + /// Gets a over the string's characters. + /// A over the unmanaged UTF-16 characters. + public ReadOnlySpan GetSpan() => Native.GetSpan(); + + public static implicit operator ReadOnlySpan(Utf16UnmanagedString unmanaged) + => unmanaged.Native; + + public static implicit operator string(Utf16UnmanagedString unmanaged) + => unmanaged.Native.Length == 0 + ? "" + : unmanaged.Native.ToString(); + + /// + public override string ToString() => Native.ToString(); + + private static Utf16UnmanagedString TransformUtf8ToUnicode( + ReadOnlySpan source, + Span target) + { + try + { + ref byte sourceRef = ref MemoryMarshal.GetReference(source); + ref char targetRef = ref MemoryMarshal.GetReference(target); + + byte* sourceP = (byte*)Unsafe.AsPointer(ref sourceRef); + char* targetP = (char*)Unsafe.AsPointer(ref targetRef); + + int written = Encoding.UTF8.GetChars(sourceP, source.Length, targetP, target.Length); + return CreateFromManaged(target[..written]); + } + catch (Exception ex) + { + throw ExceptionHelper.ThrowHDiffStringEncodingFailed(ex); + } + } + + /// Represents a non-owning pointer and length for a UTF-16 string. + /// A pointer to the UTF-16 characters. + /// The number of characters. + [StructLayout(LayoutKind.Sequential)] + public readonly struct NativeStringW(char* chars, int length) + { + /// Gets an empty . + public static NativeStringW Empty => default; + + /// The address of the first UTF-16 character. + public readonly nint Chars = (nint)chars; + /// The number of UTF-16 characters. + public readonly int Length = length; + + public static implicit operator ReadOnlySpan(NativeStringW unmanaged) + => new((char*)unmanaged.Chars, unmanaged.Length); + + /// + public override string ToString() => new((char*)Chars, 0, Length); + + public static implicit operator string(NativeStringW unmanaged) + => unmanaged.Length == 0 + ? "" + : unmanaged.ToString(); + + /// Gets a over the native characters. + /// A over the native characters. + public ReadOnlySpan GetSpan() => this; + } +} diff --git a/SharpHDiffPatch.Core/Binary/Compression/BZip2/BZip2Constants.cs b/SharpHPatchZ/IO/Compression/BZip2/BZip2Constants.cs similarity index 98% rename from SharpHDiffPatch.Core/Binary/Compression/BZip2/BZip2Constants.cs rename to SharpHPatchZ/IO/Compression/BZip2/BZip2Constants.cs index 6e7a929..734e8ba 100644 --- a/SharpHDiffPatch.Core/Binary/Compression/BZip2/BZip2Constants.cs +++ b/SharpHPatchZ/IO/Compression/BZip2/BZip2Constants.cs @@ -1,6 +1,6 @@ -using System; +using System; -namespace SharpHDiffPatch.Core.Binary.Compression.BZip2; +namespace SharpHPatchZ.IO.Compression.BZip2; /// /// Defines internal values for both compression and decompression diff --git a/SharpHDiffPatch.Core/Binary/Compression/BZip2/BZip2Crc32.cs b/SharpHPatchZ/IO/Compression/BZip2/BZip2Crc32.cs similarity index 95% rename from SharpHDiffPatch.Core/Binary/Compression/BZip2/BZip2Crc32.cs rename to SharpHPatchZ/IO/Compression/BZip2/BZip2Crc32.cs index 8517680..21ea110 100644 --- a/SharpHDiffPatch.Core/Binary/Compression/BZip2/BZip2Crc32.cs +++ b/SharpHPatchZ/IO/Compression/BZip2/BZip2Crc32.cs @@ -10,7 +10,7 @@ // ReSharper disable InconsistentNaming #endif -namespace SharpHDiffPatch.Core.Binary.Compression.BZip2; +namespace SharpHPatchZ.IO.Compression.BZip2; file static class BZip2Crc32Premul { @@ -371,10 +371,12 @@ internal static uint UpdateByte(uint crc, byte value) } } +/// Computes the big-endian CRC-32 variant used by BZip2 streams. public sealed class BZip2Crc32 : NonCryptographicHashAlgorithm { private uint _crc = BZip2Crc32Premul.InitialState; + /// Initializes a new . public BZip2Crc32() : base(BZip2Crc32Premul.HashSize) { @@ -386,22 +388,32 @@ private BZip2Crc32(uint crc) _crc = crc; } + /// Creates a copy with the same accumulated CRC state. + /// A new accumulator containing the current state. public BZip2Crc32 Clone() => new(_crc); + /// Appends a single byte to the CRC calculation. + /// The to append. public void AppendByte(byte value) => _crc = BZip2Crc32Premul.UpdateByte(_crc, value); + /// public override void Append(ReadOnlySpan source) => _crc = Update(_crc, source); + /// public override void Reset() => _crc = BZip2Crc32Premul.InitialState; + /// protected override void GetCurrentHashCore(Span destination) => BinaryPrimitives.WriteUInt32BigEndian(destination, ~_crc); + /// protected override void GetHashAndResetCore(Span destination) { BinaryPrimitives.WriteUInt32BigEndian(destination, ~_crc); _crc = BZip2Crc32Premul.InitialState; } + /// Gets the current CRC as an unsigned 32-bit integer without resetting the accumulator. + /// The current CRC value. public uint GetCurrentHashAsUInt32() => ~_crc; private static uint Update(uint crc, ReadOnlySpan source) @@ -418,4 +430,4 @@ private static uint Update(uint crc, ReadOnlySpan source) return BZip2Crc32Premul.UpdateSlicingBy8(crc, source); } -} \ No newline at end of file +} diff --git a/SharpHDiffPatch.Core/Binary/Compression/BZip2/BZip2Exception.cs b/SharpHPatchZ/IO/Compression/BZip2/BZip2Exception.cs similarity index 96% rename from SharpHDiffPatch.Core/Binary/Compression/BZip2/BZip2Exception.cs rename to SharpHPatchZ/IO/Compression/BZip2/BZip2Exception.cs index 7e09d4c..5a9ccfe 100644 --- a/SharpHDiffPatch.Core/Binary/Compression/BZip2/BZip2Exception.cs +++ b/SharpHPatchZ/IO/Compression/BZip2/BZip2Exception.cs @@ -1,7 +1,7 @@ using System; using System.Runtime.Serialization; -namespace SharpHDiffPatch.Core.Binary.Compression.BZip2; +namespace SharpHPatchZ.IO.Compression.BZip2; /// /// BZip2Exception represents exceptions specific to BZip2 classes and code. diff --git a/SharpHDiffPatch.Core/Binary/Compression/BZip2/BZip2InputStream.cs b/SharpHPatchZ/IO/Compression/BZip2/BZip2InputStream.cs similarity index 97% rename from SharpHDiffPatch.Core/Binary/Compression/BZip2/BZip2InputStream.cs rename to SharpHPatchZ/IO/Compression/BZip2/BZip2InputStream.cs index 5b2883c..83fdad5 100644 --- a/SharpHDiffPatch.Core/Binary/Compression/BZip2/BZip2InputStream.cs +++ b/SharpHPatchZ/IO/Compression/BZip2/BZip2InputStream.cs @@ -6,7 +6,7 @@ // ReSharper disable CommentTypo -namespace SharpHDiffPatch.Core.Binary.Compression.BZip2; +namespace SharpHPatchZ.IO.Compression.BZip2; /// /// An input stream that decompresses files in the BZip2 format. @@ -49,10 +49,10 @@ public sealed class BZip2InputStream : Stream private byte[] _ll8 = []; private ushort[] _perm; - private readonly int[] _unZfTab; - private readonly int[] _limit = new int[BZip2Constants.GroupCount * CodeTableStride]; - private readonly int[] _baseArray = new int[BZip2Constants.GroupCount * CodeTableStride]; - private readonly byte[] _minLens = new byte[BZip2Constants.GroupCount]; + private readonly int[] _unZfTab; + private readonly int[] _limit = new int[BZip2Constants.GroupCount * CodeTableStride]; + private readonly int[] _baseArray = new int[BZip2Constants.GroupCount * CodeTableStride]; + private readonly byte[] _minLens = new byte[BZip2Constants.GroupCount]; private bool _streamEnd; private bool _blockEndPending; @@ -88,6 +88,10 @@ private static long TryGetStreamLength(Stream stream) } } + /// Initializes a new . + /// The containing BZip2-compressed data. + /// Whether to decompress concatenated BZip2 streams. + /// Whether to leave open when this stream is disposed. public BZip2InputStream(Stream stream, bool decompressConcatenated, bool leaveOpen = false) { if (stream == null!) @@ -135,6 +139,7 @@ streamLength is > 0 and < InputBufferSize } } + /// protected override void Dispose(bool disposing) { if (_disposed) @@ -168,29 +173,41 @@ private void ReturnPooledBuffers() if (_seqToUnseq.Length != 0) ArrayPool.Shared.Return(_seqToUnseq); } + /// Gets or sets whether this instance may dispose the underlying . public bool IsStreamOwner { get; set; } = true; + /// public override bool CanRead => _baseStream.CanRead; + /// public override bool CanSeek => false; + /// public override bool CanWrite => false; + /// public override long Length => _baseStream.Length; + /// public override long Position { get => throw new NotSupportedException("Cannot get the position of the compressed data"); set => throw new NotSupportedException("BZip2InputStream position cannot be set"); } + /// public override void Flush() => _baseStream.Flush(); + /// public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException("BZip2InputStream Seek not supported"); + /// public override void SetLength(long value) => throw new NotSupportedException("BZip2InputStream SetLength not supported"); + /// public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException("BZip2InputStream Write not supported"); + /// public override void WriteByte(byte value) => throw new NotSupportedException("BZip2InputStream WriteByte not supported"); + /// public override int Read(byte[] buffer, int offset, int count) { if (buffer == null) @@ -209,6 +226,7 @@ public override int Read(byte[] buffer, int offset, int count) } #if NET6_0_OR_GREATER + /// public override int Read(Span buffer) => ReadCore(buffer); #endif @@ -274,6 +292,7 @@ private int ReadCore(Span destination) return written; } + /// public override int ReadByte() { if (_streamEnd) @@ -1318,4 +1337,4 @@ private void HbCreateDecodeTables( [MethodImpl(MethodImplOptions.NoInlining)] private static void DataError() => throw new BZip2Exception("Bzip data error"); -} \ No newline at end of file +} diff --git a/SharpHDiffPatch.Core/Binary/Compression/BZip2/LICENSE b/SharpHPatchZ/IO/Compression/BZip2/LICENSE similarity index 100% rename from SharpHDiffPatch.Core/Binary/Compression/BZip2/LICENSE rename to SharpHPatchZ/IO/Compression/BZip2/LICENSE diff --git a/SharpHDiffPatch.Core/Binary/Compression/BZip2/README.md b/SharpHPatchZ/IO/Compression/BZip2/README.md similarity index 100% rename from SharpHDiffPatch.Core/Binary/Compression/BZip2/README.md rename to SharpHPatchZ/IO/Compression/BZip2/README.md diff --git a/SharpHPatchZ/IO/Compression/CompressionStreamHelper.cs b/SharpHPatchZ/IO/Compression/CompressionStreamHelper.cs new file mode 100644 index 0000000..6947eaa --- /dev/null +++ b/SharpHPatchZ/IO/Compression/CompressionStreamHelper.cs @@ -0,0 +1,169 @@ +// ReSharper disable IdentifierTypo +// ReSharper disable ConvertSwitchStatementToSwitchExpression +// ReSharper disable CommentTypo +// ReSharper disable InconsistentNaming + +#if NET6_0_OR_GREATER && !NET11_0_OR_GREATER +using System.Collections.Generic; +using ZstdNet; +#endif + +using SharpHPatchZ.Extension; +using SharpHPatchZ.Header; +using SharpHPatchZ.IO.Compression.BZip2; +using SharpHPatchZ.IO.Compression.Lzma; +using System; +using System.IO; +using System.IO.Compression; + +#if !NET11_0_OR_GREATER +using ZstdManagedDecompressor = ZstdSharp.Decompressor; +using ZstdManagedDecompressorParameter = ZstdSharp.Unsafe.ZSTD_dParameter; +using ZstdManagedStream = ZstdSharp.DecompressionStream; +#endif + +#if NET6_0_OR_GREATER && !NET11_0_OR_GREATER +using ZstdNativeDecompressor = ZstdNet.DecompressionOptions; +using ZstdNativeDecompressorParameter = ZstdNet.ZSTD_dParameter; +using ZstdNativeStream = ZstdNet.DecompressionStream; +#endif + +namespace SharpHPatchZ.IO.Compression; + +internal interface IDecompressor +{ + Stream CreateDecompressionStream( + Stream sourceStream, + long compressedSize, + long decompressedSize, + bool leaveOpen); +} + +internal readonly struct HDiffDecompressor(HDiffCompression type) : IDecompressor +{ + public Stream CreateDecompressionStream( + Stream sourceStream, + long compressedSize, + long decompressedSize, + bool leaveOpen) + => DecompressStreamFactory.Create(type, sourceStream, leaveOpen, compressedSize, decompressedSize); +} + +internal static class DecompressStreamFactory +{ + private delegate Stream ZstdStreamFallback(Stream stream, bool leaveOpen); + private static ZstdStreamFallback? _createZstdStreamFallback; + private static readonly int ZstdWindowLogMax = Environment.Is64BitProcess ? 31 : 30; + + internal static Stream Create(HDiffCompression type, + Stream sourceStream, + bool leaveOpen, + long compressedSize = -1, + long decompressedSize = -1) + { + if (compressedSize < -1) + throw new ArgumentOutOfRangeException(nameof(compressedSize)); + + if (decompressedSize < -1) + throw new ArgumentOutOfRangeException(nameof(decompressedSize)); + + if (type == HDiffCompression.Uncompressed || compressedSize == 0) + return sourceStream; + + // Advance one byte padding for Zlib / Libdeflate + if (type == HDiffCompression.Zlib) + sourceStream.ReadByte(); + + return type switch + { + HDiffCompression.Zstd => CreateZstdStream(sourceStream, leaveOpen), + HDiffCompression.Zlib => new DeflateStream(sourceStream, CompressionMode.Decompress, leaveOpen), + HDiffCompression.BZ2 => new BZip2InputStream(sourceStream, false, leaveOpen), + HDiffCompression.PBZ2 => new BZip2InputStream(sourceStream, true, leaveOpen), + HDiffCompression.Lzma or HDiffCompression.Lzma2 => CreateLzmaStream(type, sourceStream, compressedSize, decompressedSize, leaveOpen), + _ => throw ExceptionHelper.ThrowHDiffHeaderCompressionNotSupported(type.ToString()) + }; + } + + private static Stream CreateZstdStream(Stream rawStream, bool leaveOpen) + { + if (_createZstdStreamFallback != null) return _createZstdStreamFallback(rawStream, leaveOpen); + +#if NET6_0_OR_GREATER && !NET11_0_OR_GREATER + if (DllUtils.IsLibraryExist(DllUtils.DllName)) + _createZstdStreamFallback = CreateZstdNativeStream; + else + _createZstdStreamFallback = CreateZstdManagedStream; +#elif NET11_0_OR_GREATER + _createZstdStreamFallback = CreateZstdNativeStream; +#else + _createZstdStreamFallback = CreateZstdManagedStream; +#endif + return _createZstdStreamFallback(rawStream, leaveOpen); + } + + /* HACK: The default window log max size is 30. This is unacceptable since the native HPatch implementation + * always use 31 as the size_t, which is 8 bytes length. + * + * Code Snippets (decompress_plugin_demo.h:963): + * #define _ZSTD_WINDOWLOG_MAX ((sizeof(size_t)<=4)?30:31) + */ +#if NET6_0_OR_GREATER && !NET11_0_OR_GREATER + private static Stream CreateZstdNativeStream(Stream rawStream, bool leaveOpen) => + new ZstdNativeStream(rawStream, new ZstdNativeDecompressor(null, new Dictionary + { + { ZstdNativeDecompressorParameter.ZSTD_d_windowLogMax, ZstdWindowLogMax } + }), 0, leaveOpen); +#elif NET11_0_OR_GREATER + private static Stream CreateZstdNativeStream(Stream rawStream, bool leaveOpen) => + new ZstandardStream(rawStream, new ZstandardDecompressionOptions + { + MaxWindowLog2 = ZstdWindowLogMax + }, leaveOpen); +#endif + +#if !NET11_0_OR_GREATER + private static Stream CreateZstdManagedStream(Stream rawStream, bool leaveOpen) + { + ZstdManagedDecompressor decompressor = new(); + decompressor.SetParameter(ZstdManagedDecompressorParameter.ZSTD_d_windowLogMax, ZstdWindowLogMax); + return new ZstdManagedStream(rawStream, decompressor, 16 << 10, leaveOpen: leaveOpen); + } +#endif + + private static Stream CreateLzmaStream( + HDiffCompression type, + Stream rawStream, + long compressedSize, + long decompressedSize, + bool leaveOpen) + { + int property = rawStream.ReadByte(); + if (property < 0) + throw ExceptionHelper.ThrowHDiffCompLZMAPropertyMissing(); + + long payloadSize = compressedSize >= 0 ? compressedSize - 1 : -1; + if (type == HDiffCompression.Lzma2) + { + if (property > 40) + throw ExceptionHelper.ThrowHDiffCompLZMA2DictionaryInvalid(property); + return payloadSize == 0 + ? throw ExceptionHelper.ThrowHDiffCompLZMA2NoCompressedPayload() + : new LzmaInputStream([(byte)property], rawStream, payloadSize, decompressedSize, leaveOpen); + } + + const int lzmaPropertySize = 5; + if (property != lzmaPropertySize) + throw ExceptionHelper.ThrowHDiffCompLZMADictionaryInvalidLength(lzmaPropertySize, property); + + const int rangeDecoderHeaderSize = 5; + if (compressedSize is >= 0 and < 1 + lzmaPropertySize + rangeDecoderHeaderSize) + throw ExceptionHelper.ThrowHDiffCompLZMASizeTooSmallForDictionaryRead(); + + byte[] properties = new byte[lzmaPropertySize]; + rawStream.ReadExactly(properties, 0, properties.Length); + payloadSize = compressedSize >= 0 ? compressedSize - 1 - properties.Length : -1; + + return new LzmaInputStream(properties, rawStream, payloadSize, decompressedSize, leaveOpen); + } +} diff --git a/SharpHDiffPatch.Core/Binary/Compression/Lzma/LICENSE.txt b/SharpHPatchZ/IO/Compression/Lzma/LICENSE.txt similarity index 100% rename from SharpHDiffPatch.Core/Binary/Compression/Lzma/LICENSE.txt rename to SharpHPatchZ/IO/Compression/Lzma/LICENSE.txt diff --git a/SharpHDiffPatch.Core/Binary/Compression/Lzma/LZ/LzOutWindow.cs b/SharpHPatchZ/IO/Compression/Lzma/LZ/LzOutWindow.cs similarity index 69% rename from SharpHDiffPatch.Core/Binary/Compression/Lzma/LZ/LzOutWindow.cs rename to SharpHPatchZ/IO/Compression/Lzma/LZ/LzOutWindow.cs index 913c8d9..340cf8b 100644 --- a/SharpHDiffPatch.Core/Binary/Compression/Lzma/LZ/LzOutWindow.cs +++ b/SharpHPatchZ/IO/Compression/Lzma/LZ/LzOutWindow.cs @@ -1,32 +1,36 @@ -using System; +using System; using System.Buffers; using System.IO; using System.Runtime.CompilerServices; +using SharpHPatchZ.Extension; -namespace SharpHDiffPatch.Core.Binary.Compression.Lzma.LZ; +namespace SharpHPatchZ.IO.Compression.Lzma.LZ; internal class OutWindow : IDisposable { - private byte[] _buffer = []; - private int _windowSize; - private int _pos; - private int _streamPos; - private int _pendingLen; - private int _pendingDist; - private Stream _stream; + private NativeMemoryBuffer? _buffer; +#if !NET6_0_OR_GREATER + private byte[] _streamBuffer = []; +#endif + private int _windowSize; + private int _pos; + private int _streamPos; + private int _pendingLen; + private int _pendingDist; + private Stream? _stream; public long Total; public long Limit; public void Create(int windowSize) { - if (_buffer.Length < windowSize) + if (_buffer is null || _buffer.Length < windowSize) { - ReturnPooledBuffer(); - _buffer = ArrayPool.Shared.Rent(windowSize); + ReleaseBuffer(); + _buffer = new NativeMemoryBuffer(windowSize); } - _buffer[windowSize - 1] = 0; + Unsafe.Add(ref _buffer.GetReference(), windowSize - 1) = 0; _windowSize = windowSize; _pos = 0; _streamPos = 0; @@ -79,7 +83,21 @@ public void Flush() return; } - _stream.Write(_buffer, _streamPos, size); +#if NET6_0_OR_GREATER + _stream.Write(_buffer!.Span.Slice(_streamPos, size)); +#else + byte[] streamBuffer = GetStreamBuffer(); + int sourceOffset = _streamPos; + int remaining = size; + while (remaining > 0) + { + int step = Math.Min(remaining, streamBuffer.Length); + _buffer!.Span.Slice(sourceOffset, step).CopyTo(streamBuffer); + _stream.Write(streamBuffer, 0, step); + sourceOffset += step; + remaining -= step; + } +#endif if (_pos >= _windowSize) { _pos = 0; @@ -106,7 +124,7 @@ public void CopyBlock(int distance, int len) copySize = (int)available; } - ref byte buffer = ref _buffer[0]; + ref byte buffer = ref _buffer!.GetReference(); int beforeWrap = Math.Min(copySize, _windowSize - source); CopyBytes(ref buffer, source, _pos, beforeWrap); source += beforeWrap; @@ -161,7 +179,7 @@ private static unsafe void CopyBytes(ref byte buffer, int sourceOffset, int dest public void PutByte(byte b) { - _buffer[_pos++] = b; + Unsafe.Add(ref _buffer!.GetReference(), _pos++) = b; Total++; if (_pos >= _windowSize) @@ -177,7 +195,7 @@ public byte GetByte(int distance) { pos += _windowSize; } - return _buffer[pos]; + return Unsafe.Add(ref _buffer!.GetReference(), pos); } public int CopyStream(Stream stream, int len) @@ -196,7 +214,13 @@ public int CopyStream(Stream stream, int len) curSize = size; } - int numReadBytes = stream.Read(_buffer, _pos, curSize); +#if NET6_0_OR_GREATER + int numReadBytes = stream.Read(_buffer!.Span.Slice(_pos, curSize)); +#else + byte[] streamBuffer = GetStreamBuffer(); + int numReadBytes = stream.Read(streamBuffer, 0, Math.Min(curSize, streamBuffer.Length)); + streamBuffer.AsSpan(0, numReadBytes).CopyTo(_buffer!.Span.Slice(_pos, numReadBytes)); +#endif if (numReadBytes == 0) { throw new LzmaDataErrorException(); @@ -232,7 +256,7 @@ public int Read(byte[] buffer, int offset, int count) size = count; } - _buffer.AsSpan(_streamPos, size).CopyTo(buffer.AsSpan(offset, size)); + _buffer!.Span.Slice(_streamPos, size).CopyTo(buffer.AsSpan(offset, size)); _streamPos += size; if (_streamPos < _windowSize) return size; @@ -254,16 +278,33 @@ public void CopyPending() public void Dispose() { ReleaseStream(); - ReturnPooledBuffer(); + ReleaseBuffer(); +#if !NET6_0_OR_GREATER + byte[] streamBuffer = _streamBuffer; + _streamBuffer = []; + if (streamBuffer.Length != 0) + { + ArrayPool.Shared.Return(streamBuffer); + } +#endif } - private void ReturnPooledBuffer() + private void ReleaseBuffer() { - byte[] buffer = _buffer; - _buffer = []; - if (buffer.Length != 0) + NativeMemoryBuffer? buffer = _buffer; + _buffer = null; + buffer?.Dispose(); + } + +#if !NET6_0_OR_GREATER + private byte[] GetStreamBuffer() + { + if (_streamBuffer.Length == 0) { - ArrayPool.Shared.Return(buffer); + _streamBuffer = ArrayPool.Shared.Rent(64 << 10); } + + return _streamBuffer; } -} \ No newline at end of file +#endif +} diff --git a/SharpHDiffPatch.Core/Binary/Compression/Lzma/LzmaBase.cs b/SharpHPatchZ/IO/Compression/Lzma/LzmaBase.cs similarity index 97% rename from SharpHDiffPatch.Core/Binary/Compression/Lzma/LzmaBase.cs rename to SharpHPatchZ/IO/Compression/Lzma/LzmaBase.cs index 72f1650..21885fa 100644 --- a/SharpHDiffPatch.Core/Binary/Compression/Lzma/LzmaBase.cs +++ b/SharpHPatchZ/IO/Compression/Lzma/LzmaBase.cs @@ -1,4 +1,4 @@ -namespace SharpHDiffPatch.Core.Binary.Compression.Lzma; +namespace SharpHPatchZ.IO.Compression.Lzma; internal abstract class Base { diff --git a/SharpHDiffPatch.Core/Binary/Compression/Lzma/LzmaDecoder.cs b/SharpHPatchZ/IO/Compression/Lzma/LzmaDecoder.cs similarity index 97% rename from SharpHDiffPatch.Core/Binary/Compression/Lzma/LzmaDecoder.cs rename to SharpHPatchZ/IO/Compression/Lzma/LzmaDecoder.cs index 14bffdf..6572f83 100644 --- a/SharpHDiffPatch.Core/Binary/Compression/Lzma/LzmaDecoder.cs +++ b/SharpHPatchZ/IO/Compression/Lzma/LzmaDecoder.cs @@ -1,11 +1,10 @@ -using System; +using System; using System.Buffers; -using System.IO; using System.Runtime.CompilerServices; -using SharpHDiffPatch.Core.Binary.Compression.Lzma.LZ; -using SharpHDiffPatch.Core.Binary.Compression.Lzma.RangeCoder; +using SharpHPatchZ.IO.Compression.Lzma.LZ; +using SharpHPatchZ.IO.Compression.Lzma.RangeCoder; -namespace SharpHDiffPatch.Core.Binary.Compression.Lzma; +namespace SharpHPatchZ.IO.Compression.Lzma; internal class Decoder : IDisposable { @@ -248,10 +247,10 @@ private void Init() } public void Code( - Stream inStream, - Stream outStream, - long inSize, - long outSize) + System.IO.Stream inStream, + System.IO.Stream outStream, + long inSize, + long outSize) { if (_outWindow is null) { @@ -433,7 +432,7 @@ public void SetDecoderProperties(byte[] properties) } } - public void Train(Stream stream) + public void Train(System.IO.Stream stream) { if (_outWindow is null) { diff --git a/SharpHDiffPatch.Core/Binary/Compression/Lzma/LzmaExceptions.cs b/SharpHPatchZ/IO/Compression/Lzma/LzmaExceptions.cs similarity index 84% rename from SharpHDiffPatch.Core/Binary/Compression/Lzma/LzmaExceptions.cs rename to SharpHPatchZ/IO/Compression/Lzma/LzmaExceptions.cs index 9f02974..fc87f87 100644 --- a/SharpHDiffPatch.Core/Binary/Compression/Lzma/LzmaExceptions.cs +++ b/SharpHPatchZ/IO/Compression/Lzma/LzmaExceptions.cs @@ -1,6 +1,6 @@ -using System; +using System; -namespace SharpHDiffPatch.Core.Binary.Compression.Lzma; +namespace SharpHPatchZ.IO.Compression.Lzma; /// /// The exception that is thrown when an error in input stream occurs during decoding. diff --git a/SharpHDiffPatch.Core/Binary/Compression/Lzma/LzmaInputStream.cs b/SharpHPatchZ/IO/Compression/Lzma/LzmaInputStream.cs similarity index 70% rename from SharpHDiffPatch.Core/Binary/Compression/Lzma/LzmaInputStream.cs rename to SharpHPatchZ/IO/Compression/Lzma/LzmaInputStream.cs index 7b1b5c2..ab75a4a 100644 --- a/SharpHDiffPatch.Core/Binary/Compression/Lzma/LzmaInputStream.cs +++ b/SharpHPatchZ/IO/Compression/Lzma/LzmaInputStream.cs @@ -1,11 +1,12 @@ -using System; +using System; using System.Buffers.Binary; using System.IO; -using SharpHDiffPatch.Core.Binary.Compression.Lzma.LZ; -using SharpHDiffPatch.Core.Binary.Compression.Lzma.RangeCoder; +using SharpHPatchZ.IO.Compression.Lzma.LZ; +using SharpHPatchZ.IO.Compression.Lzma.RangeCoder; -namespace SharpHDiffPatch.Core.Binary.Compression.Lzma; +namespace SharpHPatchZ.IO.Compression.Lzma; +/// Provides a forward-only that decompresses LZMA or LZMA2 data while it is read. public sealed class LzmaInputStream : Stream { private readonly Stream _inputStream; @@ -15,7 +16,7 @@ public sealed class LzmaInputStream : Stream private readonly int _dictionarySize; private readonly OutWindow _outWindow = new(); private readonly RangeDecoder _rangeDecoder = new(); - private Decoder _decoder; + private Decoder? _decoder; private long _position; private bool _endReached; @@ -32,23 +33,46 @@ public sealed class LzmaInputStream : Stream private bool _needProps = true; private bool _isDisposed; + /// Initializes a new whose compressed and decompressed sizes are unknown. + /// The LZMA or LZMA2 property bytes. + /// The containing compressed data. + /// Whether to leave open when this stream is disposed. public LzmaInputStream(byte[] properties, Stream inputStream, bool leaveOpen = false) : this(properties, inputStream, -1, -1, null, properties.Length < 5, leaveOpen) { } + /// Initializes a new with a known compressed size. + /// The LZMA or LZMA2 property bytes. + /// The containing compressed data. + /// The compressed data size, in bytes. + /// Whether to leave open when this stream is disposed. public LzmaInputStream(byte[] properties, Stream inputStream, long inputSize, bool leaveOpen = false) : this(properties, inputStream, inputSize, -1, null, properties.Length < 5, leaveOpen) { } + /// Initializes a new with known compressed and decompressed sizes. + /// The LZMA or LZMA2 property bytes. + /// The containing compressed data. + /// The compressed data size, in bytes. + /// The decompressed data size, in bytes. + /// Whether to leave open when this stream is disposed. public LzmaInputStream(byte[] properties, Stream inputStream, long inputSize, long outputSize, bool leaveOpen = false) : this(properties, inputStream, inputSize, outputSize, null, properties.Length < 5, leaveOpen) { } + /// Initializes a new with explicit format and dictionary settings. + /// The decoder property bytes. + /// The containing compressed data. + /// The compressed data size, in bytes, or a negative value when unknown. + /// The decompressed data size, in bytes, or a negative value when unknown. + /// An optional preset dictionary . + /// Whether the compressed data uses the LZMA2 container format. + /// Whether to leave open when this stream is disposed. public LzmaInputStream( - byte[] properties, - Stream inputStream, - long inputSize, - long outputSize, - Stream presetDictionary, - bool isLzma2, - bool leaveOpen = false) + byte[] properties, + Stream inputStream, + long inputSize, + long outputSize, + Stream? presetDictionary, + bool isLzma2, + bool leaveOpen = false) { _inputStream = inputStream; _inputSize = inputSize; @@ -91,14 +115,19 @@ public LzmaInputStream( } } + /// public override bool CanRead => true; + /// public override bool CanSeek => false; + /// public override bool CanWrite => false; + /// public override void Flush() { } + /// protected override void Dispose(bool disposing) { if (_isDisposed) @@ -113,19 +142,22 @@ protected override void Dispose(bool disposing) if (disposing && !_leaveOpen) { - _inputStream?.Dispose(); + _inputStream.Dispose(); } base.Dispose(disposing); } + /// public override long Length => _position + _availableBytes; + /// public override long Position { get => _position; set => throw new NotSupportedException(); } + /// public override int Read(byte[] buffer, int offset, int count) { if (_endReached) @@ -163,7 +195,7 @@ public override int Read(byte[] buffer, int offset, int count) { _inputPosition += _outWindow.CopyStream(_inputStream, toProcess); } - else if (_decoder.Code(_dictionarySize, _outWindow, _rangeDecoder) && _outputSize < 0) + else if (_decoder != null && _decoder.Code(_dictionarySize, _outWindow, _rangeDecoder) && _outputSize < 0) { _availableBytes = _outWindow.AvailableBytes; } @@ -181,7 +213,7 @@ public override int Read(byte[] buffer, int offset, int count) { // Stream might have End Of Stream marker _outWindow.SetLimit(toProcess + 1); - if (!_decoder.Code(_dictionarySize, _outWindow, _rangeDecoder)) + if (_decoder != null && !_decoder.Code(_dictionarySize, _outWindow, _rangeDecoder)) { _rangeDecoder.ReleaseStream(); throw new LzmaDataErrorException(); @@ -277,11 +309,15 @@ private void DecodeChunkHeader() } } + /// public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + /// public override void SetLength(long value) => throw new NotSupportedException(); + /// public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + /// Gets the decoder property bytes used by this stream. public byte[] Properties { get; } -} \ No newline at end of file +} diff --git a/SharpHDiffPatch.Core/Binary/Compression/Lzma/README.md b/SharpHPatchZ/IO/Compression/Lzma/README.md similarity index 100% rename from SharpHDiffPatch.Core/Binary/Compression/Lzma/README.md rename to SharpHPatchZ/IO/Compression/Lzma/README.md diff --git a/SharpHDiffPatch.Core/Binary/Compression/Lzma/RangeCoder/RangeCoder.cs b/SharpHPatchZ/IO/Compression/Lzma/RangeCoder/RangeCoder.cs similarity index 73% rename from SharpHDiffPatch.Core/Binary/Compression/Lzma/RangeCoder/RangeCoder.cs rename to SharpHPatchZ/IO/Compression/Lzma/RangeCoder/RangeCoder.cs index 784fc12..8fdbfbf 100644 --- a/SharpHDiffPatch.Core/Binary/Compression/Lzma/RangeCoder/RangeCoder.cs +++ b/SharpHPatchZ/IO/Compression/Lzma/RangeCoder/RangeCoder.cs @@ -1,9 +1,12 @@ -using System; +using System; using System.Buffers; using System.IO; using System.Runtime.CompilerServices; +#if NET6_0_OR_GREATER +using SharpHPatchZ.Extension; +#endif -namespace SharpHDiffPatch.Core.Binary.Compression.Lzma.RangeCoder; +namespace SharpHPatchZ.IO.Compression.Lzma.RangeCoder; internal class RangeDecoder : IDisposable { @@ -15,11 +18,15 @@ internal class RangeDecoder : IDisposable public Stream Stream; public long Total; - private byte[] _inputBuffer = []; - private int _inputOffset; - private int _inputCount; - private long _inputLimit; - private bool _useInputBuffer; +#if NET6_0_OR_GREATER + private NativeMemoryBuffer? _inputBuffer; +#else + private byte[] _inputBuffer = []; +#endif + private int _inputOffset; + private int _inputCount; + private long _inputLimit; + private bool _useInputBuffer; public void Init(Stream stream, long inputLimit = -1) { @@ -96,7 +103,11 @@ internal uint ReadByte() } Total++; +#if NET6_0_OR_GREATER + return Unsafe.Add(ref _inputBuffer!.GetReference(), _inputOffset++); +#else return _inputBuffer[_inputOffset++]; +#endif } [MethodImpl(MethodImplOptions.NoInlining)] @@ -108,13 +119,19 @@ private void FillInputBuffer() throw new LzmaDataErrorException(); } +#if NET6_0_OR_GREATER + _inputBuffer ??= new NativeMemoryBuffer(InputBufferSize); + int requested = (int)Math.Min(_inputBuffer.Length, remaining); + _inputCount = Stream.Read(_inputBuffer.Span[..requested]); +#else if (_inputBuffer.Length == 0) { _inputBuffer = ArrayPool.Shared.Rent(InputBufferSize); } int requested = (int)Math.Min(_inputBuffer.Length, remaining); - _inputCount = Stream.Read(_inputBuffer, 0, requested); + _inputCount = Stream.Read(_inputBuffer, 0, requested); +#endif _inputOffset = 0; if (_inputCount <= 0) { @@ -125,6 +142,11 @@ private void FillInputBuffer() public void Dispose() { ReleaseStream(); +#if NET6_0_OR_GREATER + NativeMemoryBuffer? inputBuffer = _inputBuffer; + _inputBuffer = null; + inputBuffer?.Dispose(); +#else byte[] inputBuffer = _inputBuffer; _inputBuffer = []; @@ -132,5 +154,6 @@ public void Dispose() { ArrayPool.Shared.Return(inputBuffer); } +#endif } -} \ No newline at end of file +} diff --git a/SharpHDiffPatch.Core/Binary/Compression/Lzma/RangeCoder/RangeCoderBit.cs b/SharpHPatchZ/IO/Compression/Lzma/RangeCoder/RangeCoderBit.cs similarity index 95% rename from SharpHDiffPatch.Core/Binary/Compression/Lzma/RangeCoder/RangeCoderBit.cs rename to SharpHPatchZ/IO/Compression/Lzma/RangeCoder/RangeCoderBit.cs index d643876..2669635 100644 --- a/SharpHDiffPatch.Core/Binary/Compression/Lzma/RangeCoder/RangeCoderBit.cs +++ b/SharpHPatchZ/IO/Compression/Lzma/RangeCoder/RangeCoderBit.cs @@ -1,7 +1,7 @@ -using System; +using System; using System.Runtime.CompilerServices; -namespace SharpHDiffPatch.Core.Binary.Compression.Lzma.RangeCoder; +namespace SharpHPatchZ.IO.Compression.Lzma.RangeCoder; internal struct BitDecoder { diff --git a/SharpHDiffPatch.Core/Binary/Compression/Lzma/RangeCoder/RangeCoderBitTree.cs b/SharpHPatchZ/IO/Compression/Lzma/RangeCoder/RangeCoderBitTree.cs similarity index 94% rename from SharpHDiffPatch.Core/Binary/Compression/Lzma/RangeCoder/RangeCoderBitTree.cs rename to SharpHPatchZ/IO/Compression/Lzma/RangeCoder/RangeCoderBitTree.cs index b536e46..42c25a6 100644 --- a/SharpHDiffPatch.Core/Binary/Compression/Lzma/RangeCoder/RangeCoderBitTree.cs +++ b/SharpHPatchZ/IO/Compression/Lzma/RangeCoder/RangeCoderBitTree.cs @@ -1,6 +1,6 @@ -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; -namespace SharpHDiffPatch.Core.Binary.Compression.Lzma.RangeCoder; +namespace SharpHPatchZ.IO.Compression.Lzma.RangeCoder; internal readonly struct BitTreeDecoder(int numBitLevels) { diff --git a/SharpHPatchZ/IO/RandomAccessCompat.cs b/SharpHPatchZ/IO/RandomAccessCompat.cs new file mode 100644 index 0000000..1960d59 --- /dev/null +++ b/SharpHPatchZ/IO/RandomAccessCompat.cs @@ -0,0 +1,273 @@ +using System; +using System.IO; +using Microsoft.Win32.SafeHandles; + +#if !NET6_0_OR_GREATER +using System.ComponentModel; +using System.Runtime.InteropServices; +using SharpHPatchZ.Native; +#endif +// ReSharper disable InconsistentNaming +// ReSharper disable StringLiteralTypo + +namespace SharpHPatchZ.IO; + +/// +/// Provides position-independent file I/O on all target frameworks supported by +/// SharpHPatchZ. Modern targets use the runtime implementation; .NET Standard 2.0 +/// calls the equivalent operating-system APIs directly. +/// +internal static class RandomAccessCompat +{ +#if !NET6_0_OR_GREATER + private static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + private static readonly unsafe bool UseLongLongPReadWrite = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && sizeof(nint) == 4; +#endif + + public static int Read(SafeFileHandle handle, Span buffer, long fileOffset) + { +#if NET6_0_OR_GREATER + return RandomAccess.Read(handle, buffer, fileOffset); +#else + ValidateArguments(handle, fileOffset); + + if (buffer.IsEmpty) + { + return 0; + } + + return IsWindows + ? ReadWindows(handle, buffer, fileOffset) + : ReadUnix(handle, buffer, fileOffset); +#endif + } + + public static void Write(SafeFileHandle handle, ReadOnlySpan buffer, long fileOffset) + { +#if NET6_0_OR_GREATER + RandomAccess.Write(handle, buffer, fileOffset); +#else + ValidateArguments(handle, fileOffset); + + if (IsWindows) + { + WriteWindows(handle, buffer, fileOffset); + } + else + { + WriteUnix(handle, buffer, fileOffset); + } +#endif + } + + +#if !NET6_0_OR_GREATER + private static void ValidateArguments(SafeFileHandle handle, long fileOffset) + { + if (handle is null) + { + throw new ArgumentNullException(nameof(handle)); + } + + if (handle.IsClosed || handle.IsInvalid) + { + throw new ObjectDisposedException(nameof(handle)); + } + + if (fileOffset < 0) + { + throw new ArgumentOutOfRangeException(nameof(fileOffset)); + } + } + + private const int ErrorHandleEof = 38; + private const int ErrorInterrupted = 4; + + private static int ReadWindows( + SafeFileHandle handle, + Span buffer, + long fileOffset) + { + bool addedRef = false; + try + { + handle.DangerousAddRef(ref addedRef); + PInvoke.Windows.NativeOverlappedData overlapped = new(fileOffset); + + bool success = PInvoke.Windows.ReadFile(handle.DangerousGetHandle(), + ref buffer.GetPinnableReference(), + (uint)buffer.Length, + out uint bytesRead, + ref overlapped); + if (success) + { + return checked((int)bytesRead); + } + + int error = Marshal.GetLastWin32Error(); + return error == ErrorHandleEof + ? 0 + : throw CreateIOException("ReadFile", error); + } + finally + { + if (addedRef) + { + handle.DangerousRelease(); + } + } + } + + private static void WriteWindows( + SafeFileHandle handle, + ReadOnlySpan buffer, + long fileOffset) + { + bool addedRef = false; + try + { + handle.DangerousAddRef(ref addedRef); + + while (!buffer.IsEmpty) + { + PInvoke.Windows.NativeOverlappedData overlapped = new(fileOffset); + bool success = PInvoke.Windows.WriteFile(handle.DangerousGetHandle(), + ref MemoryMarshal.GetReference(buffer), + (uint)buffer.Length, + out uint bytesWritten, + ref overlapped); + if (!success) + { + throw CreateIOException("WriteFile", Marshal.GetLastWin32Error()); + } + + if (bytesWritten == 0) + { + throw new IOException("WriteFile completed without writing any data."); + } + + int written = checked((int)bytesWritten); + buffer = buffer[written..]; + fileOffset = checked(fileOffset + written); + } + } + finally + { + if (addedRef) + { + handle.DangerousRelease(); + } + } + } + + private static int ReadUnix( + SafeFileHandle handle, + Span buffer, + long fileOffset) + { + bool addedRef = false; + try + { + handle.DangerousAddRef(ref addedRef); + int fileDescriptor = handle.DangerousGetHandle().ToInt32(); + + while (true) + { + long result = InvokePRead(fileDescriptor, + ref MemoryMarshal.GetReference(buffer), + (uint)buffer.Length, + fileOffset).ToInt64(); + if (result >= 0) + { + return checked((int)result); + } + + int error = Marshal.GetLastWin32Error(); + if (error != ErrorInterrupted) + { + throw CreateIOException("pread", error); + } + } + } + finally + { + if (addedRef) + { + handle.DangerousRelease(); + } + } + } + + private static void WriteUnix( + SafeFileHandle handle, + ReadOnlySpan buffer, + long fileOffset) + { + bool addedRef = false; + try + { + handle.DangerousAddRef(ref addedRef); + int fileDescriptor = handle.DangerousGetHandle().ToInt32(); + + while (!buffer.IsEmpty) + { + long result; + do + { + result = InvokePWrite(fileDescriptor, + ref MemoryMarshal.GetReference(buffer), + (uint)buffer.Length, + fileOffset).ToInt64(); + } + while (result < 0 && Marshal.GetLastWin32Error() == ErrorInterrupted); + + switch (result) + { + case < 0: + throw CreateIOException("pwrite", Marshal.GetLastWin32Error()); + case 0: + throw new IOException("pwrite completed without writing any data."); + } + + int written = checked((int)result); + buffer = buffer[written..]; + fileOffset = checked(fileOffset + written); + } + } + finally + { + if (addedRef) + { + handle.DangerousRelease(); + } + } + } + + private static IntPtr InvokePRead( + int fileDescriptor, + ref byte buffer, + nuint count, + long fileOffset) + { + // Linux x86 exposes the 64-bit offset ABI as pread64. Other supported + // Unix targets use a 64-bit off_t for pread. + return UseLongLongPReadWrite + ? PInvoke.Unix.PRead64(fileDescriptor, ref buffer, count, fileOffset) + : PInvoke.Unix.PRead(fileDescriptor, ref buffer, count, fileOffset); + } + + private static IntPtr InvokePWrite( + int fileDescriptor, + ref byte buffer, + nuint count, + long fileOffset) + { + return UseLongLongPReadWrite + ? PInvoke.Unix.PWrite64(fileDescriptor, ref buffer, count, fileOffset) + : PInvoke.Unix.PWrite(fileDescriptor, ref buffer, count, fileOffset); + } + + private static IOException CreateIOException(string operation, int error) + => new($"{operation} failed with native error {error}.", new Win32Exception(error)); +#endif +} diff --git a/SharpHPatchZ/IO/Reader/BittableStreamReader.cs b/SharpHPatchZ/IO/Reader/BittableStreamReader.cs new file mode 100644 index 0000000..152323f --- /dev/null +++ b/SharpHPatchZ/IO/Reader/BittableStreamReader.cs @@ -0,0 +1,731 @@ +using System; +using System.Buffers; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using SharpHPatchZ.IO.Compression; + +namespace SharpHPatchZ.IO.Reader; + +internal sealed class BittableStreamReader + : IDisposable +#if NET6_0_OR_GREATER + , IAsyncDisposable +#endif +{ + public const int KSignTagBit = 1; + public const int KByteRleType = 2; + + + private const int DefaultBufferSize = 64 << 10; + private const int MaxPackedUIntBytes = 11; + + private int _offset; + private int _bufferedLength; + + private byte[] _backedBuffer; + public Stream BackedStream { get; private set; } + + private bool _leaveOpen; + private bool _isDisposed; + + private long _consumedByteCount; + + private bool _hasUnderlyingStreamTransition; + private long _underlyingStreamTransitionLogicalEnd; + private long _underlyingStreamTransitionStart; + private long _underlyingStreamTransitionEnd; + + public int TagBitCount; + public byte Tag; + public byte PreviousByte; + + public long Offset => _consumedByteCount + _offset; + + public long OffsetUnderlyingStream + { + get + { + long logicalOffset = Offset; + if (!_hasUnderlyingStreamTransition) + { + return logicalOffset; + } + + // Compressed and decompressed bytes have no one-to-one mapping. Until + // the decoded segment is fully consumed, its source position remains at + // the compressed segment start. At the boundary it advances by the + // declared compressed length, then raw bytes continue one-to-one. + return logicalOffset < _underlyingStreamTransitionLogicalEnd + ? _underlyingStreamTransitionStart + : checked(_underlyingStreamTransitionEnd + + logicalOffset - _underlyingStreamTransitionLogicalEnd); + } + } + + internal BittableStreamReader(Stream stream, int bufferSize = -1, bool leaveOpen = false) + { + if (stream is null) + { + throw new ArgumentNullException(nameof(stream)); + } + + if (!stream.CanRead) + { + throw new ArgumentException("The stream must be readable.", nameof(stream)); + } + + if (bufferSize <= 0) + { + bufferSize = DefaultBufferSize; + } + + // A complete packed UInt64 must fit in the buffer so decoding can run + // against memory even when the value crosses a stream-read boundary. + bufferSize = Math.Max(bufferSize, MaxPackedUIntBytes); + + BackedStream = stream; + _backedBuffer = ArrayPool.Shared.Rent(bufferSize); + _leaveOpen = leaveOpen; + } + + public BittableStreamReader ContinueWithDecompressor( + IDecompressor decompressor, + long compressedLength, + long decompressedLength) + { + if (decompressor is null) + { + throw new ArgumentNullException(nameof(decompressor)); + } + + if (compressedLength < 0) + { + throw new ArgumentOutOfRangeException(nameof(compressedLength)); + } + + if (decompressedLength < 0) + { + throw new ArgumentOutOfRangeException(nameof(decompressedLength)); + } + + ThrowIfDisposed(); + ResetTag(); + + long transitionLogicalEnd = checked(Offset + decompressedLength); + long transitionUnderlyingStart = OffsetUnderlyingStream; + long transitionUnderlyingEnd = checked(transitionUnderlyingStart + compressedLength); + + byte[] replacementBuffer = ArrayPool.Shared.Rent(_backedBuffer.Length); + BufferedRemainderStream? remainderStream = null; + try + { + remainderStream = new BufferedRemainderStream(BackedStream, + _backedBuffer, + _offset, + _bufferedLength - _offset, + _leaveOpen); + + BoundedReadStream compressedStream = new(remainderStream, compressedLength); + Stream decompressedStream = decompressor.CreateDecompressionStream( + compressedStream, + compressedLength, + decompressedLength, + leaveOpen: true); + Stream transitionStream = new DecompressionTransitionStream( + decompressedStream, + compressedStream, + remainderStream, + decompressedLength); + + _consumedByteCount += _offset; + _backedBuffer = replacementBuffer; + BackedStream = transitionStream; + _offset = 0; + _bufferedLength = 0; + _leaveOpen = false; + + _hasUnderlyingStreamTransition = true; + _underlyingStreamTransitionLogicalEnd = transitionLogicalEnd; + _underlyingStreamTransitionStart = transitionUnderlyingStart; + _underlyingStreamTransitionEnd = transitionUnderlyingEnd; + return this; + } + catch + { + ArrayPool.Shared.Return(replacementBuffer); + + // Decoder construction may consume input, so the original reader can no + // longer be restored safely after a failure. Transfer cleanup to the + // remainder stream and leave this instance disposed. + _isDisposed = true; + remainderStream?.Dispose(); + throw; + } + } + + public byte ReadByte() + { + EnsureBuffered(1); + + PreviousByte = _backedBuffer[_offset++]; + TagBitCount = 0; + Tag = 0; + return PreviousByte; + } + + public async ValueTask ReadByteAsync(CancellationToken token = default) + { + await EnsureBufferedAsync(1, token).ConfigureAwait(false); + + PreviousByte = _backedBuffer[_offset++]; + TagBitCount = 0; + Tag = 0; + return PreviousByte; + } + + public void ReadBytes(byte[] buffer) + => ReadBytes(buffer, 0, buffer?.Length ?? throw new ArgumentNullException(nameof(buffer))); + + public void ReadBytes(byte[] buffer, int offset, int count) + => ReadBytes(new Span(buffer, offset, count)); + + public void ReadBytes(Memory buffer) + => ReadBytes(buffer.Span); + + public void ReadBytes(Span buffer) + { + ResetTag(); + + while (!buffer.IsEmpty) + { + EnsureBuffered(1); + + int copyLength = Math.Min(buffer.Length, _bufferedLength - _offset); + new ReadOnlySpan(_backedBuffer, _offset, copyLength).CopyTo(buffer); + + _offset += copyLength; + buffer = buffer[copyLength..]; + } + } + + public ValueTask ReadBytesAsync(byte[] buffer, CancellationToken token = default) + { + if (buffer is null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + return ReadBytesAsync(new Memory(buffer), token); + } + + public ValueTask ReadBytesAsync( + byte[] buffer, + int offset, + int count, + CancellationToken token = default) + => ReadBytesAsync(new Memory(buffer, offset, count), token); + + public async ValueTask ReadBytesAsync( + Memory buffer, + CancellationToken token = default) + { + ResetTag(); + + while (!buffer.IsEmpty) + { + await EnsureBufferedAsync(1, token).ConfigureAwait(false); + + int copyLength = Math.Min(buffer.Length, _bufferedLength - _offset); + new ReadOnlyMemory(_backedBuffer, _offset, copyLength).CopyTo(buffer); + + _offset += copyLength; + buffer = buffer[copyLength..]; + } + } + + public string ReadStringToNull() => ReadStringToNull(DefaultBufferSize); + + public string ReadStringToNull(int maxByteCount) + { + if (maxByteCount < 0) + { + throw new ArgumentOutOfRangeException(nameof(maxByteCount)); + } + + ResetTag(); + + byte[] stringBuffer = ArrayPool.Shared.Rent(Math.Max(1, Math.Min(maxByteCount, 256))); + int stringLength = 0; + try + { + while (true) + { + try + { + EnsureBuffered(1); + } + catch (EndOfStreamException exception) + { + throw new EndOfStreamException("The stream ended before the null terminator was found.", exception); + } + + int available = _bufferedLength - _offset; + int terminatorIndex = Array.IndexOf(_backedBuffer, (byte)0, _offset, available); + int bytesToCopy = terminatorIndex >= 0 ? terminatorIndex - _offset : available; + + AppendStringBytes(ref stringBuffer, + ref stringLength, + _backedBuffer, + _offset, + bytesToCopy, + maxByteCount); + + _offset += bytesToCopy; + if (terminatorIndex < 0) continue; + + ++_offset; + return Encoding.UTF8.GetString(stringBuffer, 0, stringLength); + } + } + finally + { + ArrayPool.Shared.Return(stringBuffer); + } + } + + public ValueTask ReadStringToNullAsync(CancellationToken token) + => ReadStringToNullAsync(DefaultBufferSize, token); + + public async ValueTask ReadStringToNullAsync( + int maxByteCount, + CancellationToken token = default) + { + if (maxByteCount < 0) + { + throw new ArgumentOutOfRangeException(nameof(maxByteCount)); + } + + ResetTag(); + + byte[] stringBuffer = ArrayPool.Shared.Rent(Math.Max(1, Math.Min(maxByteCount, 256))); + int stringLength = 0; + try + { + while (true) + { + try + { + await EnsureBufferedAsync(1, token).ConfigureAwait(false); + } + catch (EndOfStreamException exception) + { + throw new EndOfStreamException("The stream ended before the null terminator was found.", exception); + } + + int available = _bufferedLength - _offset; + int terminatorIndex = Array.IndexOf(_backedBuffer, (byte)0, _offset, available); + int bytesToCopy = terminatorIndex >= 0 ? terminatorIndex - _offset : available; + + AppendStringBytes( + ref stringBuffer, + ref stringLength, + _backedBuffer, + _offset, + bytesToCopy, + maxByteCount); + + _offset += bytesToCopy; + if (terminatorIndex < 0) continue; + ++_offset; + return Encoding.UTF8.GetString(stringBuffer, 0, stringLength); + } + } + finally + { + ArrayPool.Shared.Return(stringBuffer); + } + } + + public int ReadInt7Bit() + => ReadInt7Bit(0); + + public int ReadInt7Bit(int tagBit) + { + byte code = ReadFirstCode(tagBit); + return ReadInt7BitCore(code, tagBit); + } + + public int ReadInt7Bit(int tagBit, byte previousByte) + { + SetTagState(tagBit, previousByte); + return ReadInt7BitCore(previousByte, tagBit); + } + + public ValueTask ReadInt7BitAsync(CancellationToken token) + => ReadInt7BitAsync(0, token); + + public async ValueTask ReadInt7BitAsync( + int tagBit, + CancellationToken token = default) + { + byte code = await ReadFirstCodeAsync(tagBit, token); + return await ReadInt7BitCoreAsync(code, tagBit, token); + } + + public ValueTask ReadInt7BitAsync( + int tagBit, + byte previousByte, + CancellationToken token = default) + { + SetTagState(tagBit, previousByte); + return ReadInt7BitCoreAsync(previousByte, tagBit, token); + } + + public long ReadLong7Bit() + => ReadLong7Bit(0); + + public long ReadLong7Bit(int tagBit) + { + byte code = ReadFirstCode(tagBit); + return ReadLong7BitCore(code, tagBit); + } + + public long ReadLong7Bit(int tagBit, byte previousByte) + { + SetTagState(tagBit, previousByte); + return ReadLong7BitCore(previousByte, tagBit); + } + + public ValueTask ReadLong7BitAsync(CancellationToken token = default) + => ReadLong7BitAsync(0, token); + + public async ValueTask ReadLong7BitAsync( + int tagBit, + CancellationToken token = default) + { + byte code = await ReadFirstCodeAsync(tagBit, token); + return await ReadLong7BitCoreAsync(code, tagBit, token); + } + + public ValueTask ReadLong7BitAsync( + int tagBit, + byte previousByte, + CancellationToken token = default) + { + SetTagState(tagBit, previousByte); + return ReadLong7BitCoreAsync(previousByte, tagBit, token); + } + + public void ResetTag() + { + ThrowIfDisposed(); + + TagBitCount = 0; + Tag = 0; + PreviousByte = 0; + } + + private static void AppendStringBytes( + ref byte[] destination, + ref int destinationLength, + byte[] source, + int sourceOffset, + int count, + int maxByteCount) + { + if (count > maxByteCount - destinationLength) + { + throw new InvalidDataException($"The null-terminated string exceeds the maximum length of {maxByteCount} bytes."); + } + + int requiredLength = destinationLength + count; + if (requiredLength > destination.Length) + { + int newLength = destination.Length > int.MaxValue / 2 + ? int.MaxValue + : destination.Length * 2; + + byte[] replacement = ArrayPool.Shared.Rent(Math.Max(requiredLength, newLength)); + Buffer.BlockCopy(destination, 0, replacement, 0, destinationLength); + ArrayPool.Shared.Return(destination); + destination = replacement; + } + + Buffer.BlockCopy(source, sourceOffset, destination, destinationLength, count); + destinationLength = requiredLength; + } + + private int ReadInt7BitCore(byte code, int tagBit) + { + int value = code & ((1 << (7 - tagBit)) - 1); + if ((code & (1 << (7 - tagBit))) == 0) + { + return value; + } + + EnsureBuffered(MaxPackedUIntBytes - 1); + + do + { + if (value >> (sizeof(int) * 8 - 7) != 0) + { + return 0; + } + + code = ReadBufferedByte(); + value = (value << 7) | (code & ((1 << 7) - 1)); + } + while ((code & (1 << 7)) != 0); + + return value; + } + + private async ValueTask ReadInt7BitCoreAsync( + byte code, + int tagBit, + CancellationToken token) + { + int value = code & ((1 << (7 - tagBit)) - 1); + if ((code & (1 << (7 - tagBit))) == 0) + { + return value; + } + + await EnsureBufferedAsync(MaxPackedUIntBytes - 1, token); + + do + { + if (value >> (sizeof(int) * 8 - 7) != 0) + { + return 0; + } + + code = ReadBufferedByte(); + value = (value << 7) | (code & ((1 << 7) - 1)); + } + while ((code & (1 << 7)) != 0); + + return value; + } + + private long ReadLong7BitCore(byte code, int tagBit) + { + long value = code & ((1 << (7 - tagBit)) - 1); + if ((code & (1 << (7 - tagBit))) == 0) + { + return value; + } + + EnsureBuffered(MaxPackedUIntBytes - 1); + + do + { + if (value >> (sizeof(long) * 8 - 7) != 0) + { + return 0; + } + + code = ReadBufferedByte(); + value = (value << 7) | (code & (((long)1 << 7) - 1)); + } + while ((code & (1 << 7)) != 0); + + return value; + } + + private async ValueTask ReadLong7BitCoreAsync( + byte code, + int tagBit, + CancellationToken token) + { + long value = code & ((1 << (7 - tagBit)) - 1); + if ((code & (1 << (7 - tagBit))) == 0) + { + return value; + } + + await EnsureBufferedAsync(MaxPackedUIntBytes - 1, token); + + do + { + if (value >> (sizeof(long) * 8 - 7) != 0) + { + return 0; + } + + code = ReadBufferedByte(); + value = (value << 7) | (code & (((long)1 << 7) - 1)); + } + while ((code & (1 << 7)) != 0); + + return value; + } + + private byte ReadFirstCode(int tagBit) + { + ValidateTagBit(tagBit); + EnsureBuffered(MaxPackedUIntBytes); + + byte code = ReadBufferedByte(); + SetTagState(tagBit, code); + return code; + } + + private async ValueTask ReadFirstCodeAsync( + int tagBit, + CancellationToken token) + { + ValidateTagBit(tagBit); + await EnsureBufferedAsync(MaxPackedUIntBytes, token); + + byte code = ReadBufferedByte(); + SetTagState(tagBit, code); + return code; + } + + private void SetTagState(int tagBit, byte previousByte) + { + ValidateTagBit(tagBit); + ThrowIfDisposed(); + + TagBitCount = tagBit; + Tag = tagBit == 0 ? (byte)0 : (byte)(previousByte >> (8 - tagBit)); + PreviousByte = previousByte; + } + + private byte ReadBufferedByte() => _offset >= _bufferedLength + ? throw new EndOfStreamException("The stream ended in the middle of a 7-bit encoded integer.") + : _backedBuffer[_offset++]; + + private void EnsureBuffered(int minimumByteCount) + { + ThrowIfDisposed(); + + int available = _bufferedLength - _offset; + if (available >= minimumByteCount) + { + return; + } + + if (available > 0) + { + Buffer.BlockCopy(_backedBuffer, _offset, _backedBuffer, 0, available); + } + + _consumedByteCount += _offset; + _offset = 0; + _bufferedLength = available; + while (_bufferedLength < minimumByteCount) + { + int read = BackedStream.Read(_backedBuffer, + _bufferedLength, + _backedBuffer.Length - _bufferedLength); + + if (read == 0) + { + break; + } + _bufferedLength += read; + } + + if (_bufferedLength == 0) + { + throw new EndOfStreamException("Unable to read beyond the end of the stream."); + } + } + + private async ValueTask EnsureBufferedAsync( + int minimumByteCount, + CancellationToken token) + { + ThrowIfDisposed(); + + int available = _bufferedLength - _offset; + if (available >= minimumByteCount) + { + return; + } + + if (available > 0) + { + Buffer.BlockCopy(_backedBuffer, _offset, _backedBuffer, 0, available); + } + + _consumedByteCount += _offset; + _offset = 0; + _bufferedLength = available; + + while (_bufferedLength < minimumByteCount) + { +#if NET6_0_OR_GREATER + int read = await BackedStream.ReadAsync(_backedBuffer.AsMemory(_bufferedLength, _backedBuffer.Length - _bufferedLength), + token).ConfigureAwait(false); +#else + int read = await BackedStream.ReadAsync(_backedBuffer, + _bufferedLength, + _backedBuffer.Length - _bufferedLength, + token).ConfigureAwait(false); +#endif + + if (read == 0) + { + break; + } + _bufferedLength += read; + } + + if (_bufferedLength == 0) + { + throw new EndOfStreamException("Unable to read beyond the end of the stream."); + } + } + + private void ThrowIfDisposed() + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(BittableStreamReader)); + } + } + + private static void ValidateTagBit(int tagBit) + { + if ((uint)tagBit > 7) + { + throw new ArgumentOutOfRangeException(nameof(tagBit), tagBit, "The tag-bit count must be between 0 and 7."); + } + } + + public void Dispose() + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + ArrayPool.Shared.Return(_backedBuffer); + + if (!_leaveOpen) + { + BackedStream.Dispose(); + } + } + +#if NET6_0_OR_GREATER + public ValueTask DisposeAsync() + { + if (_isDisposed) + { + return ValueTask.CompletedTask; + } + + _isDisposed = true; + ArrayPool.Shared.Return(_backedBuffer); + + return _leaveOpen ? ValueTask.CompletedTask : BackedStream.DisposeAsync(); + } +#endif +} diff --git a/SharpHPatchZ/IO/Reader/DecompressionTransitionStream.cs b/SharpHPatchZ/IO/Reader/DecompressionTransitionStream.cs new file mode 100644 index 0000000..34a53cb --- /dev/null +++ b/SharpHPatchZ/IO/Reader/DecompressionTransitionStream.cs @@ -0,0 +1,492 @@ +using System; +using System.Buffers; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpHPatchZ.IO.Reader; + +internal sealed class BufferedRemainderStream : Stream +{ + private readonly Stream _stream; + private readonly bool _leaveOpen; + + private byte[]? _buffer; + private int _offset; + private readonly int _end; + private bool _isDisposed; + + internal BufferedRemainderStream( + Stream stream, + byte[] buffer, + int offset, + int count, + bool leaveOpen) + { + _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + _buffer = buffer ?? throw new ArgumentNullException(nameof(buffer)); + _offset = offset; + _end = checked(offset + count); + _leaveOpen = leaveOpen; + } + + public override bool CanRead => !_isDisposed; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => throw new NotSupportedException(); + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + ValidateArguments(buffer, offset, count); + ThrowIfDisposed(); + + int bufferedRead = ReadBuffered(new Span(buffer, offset, count)); + return bufferedRead != 0 || count == 0 + ? bufferedRead + : _stream.Read(buffer, offset, count); + } + +#if NET6_0_OR_GREATER + public override int Read(Span buffer) + { + ThrowIfDisposed(); + + int bufferedRead = ReadBuffered(buffer); + return bufferedRead != 0 || buffer.IsEmpty + ? bufferedRead + : _stream.Read(buffer); + } + + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + + int bufferedRead = ReadBuffered(buffer.Span); + return bufferedRead != 0 || buffer.IsEmpty + ? bufferedRead + : await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + } +#endif + + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken) + { + ValidateArguments(buffer, offset, count); + ThrowIfDisposed(); + + int bufferedRead = ReadBuffered(new Span(buffer, offset, count)); + return bufferedRead != 0 || count == 0 + ? bufferedRead + : await _stream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + } + + private int ReadBuffered(Span destination) + { + byte[]? source = _buffer; + if (source is null) + { + return 0; + } + + int count = Math.Min(destination.Length, _end - _offset); + new ReadOnlySpan(source, _offset, count).CopyTo(destination); + _offset += count; + + if (_offset == _end) + { + _buffer = null; + ArrayPool.Shared.Return(source); + } + + return count; + } + + protected override void Dispose(bool disposing) + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + byte[]? buffer = _buffer; + _buffer = null; + if (buffer is not null) + { + ArrayPool.Shared.Return(buffer); + } + + if (disposing && !_leaveOpen) + { + _stream.Dispose(); + } + + base.Dispose(disposing); + } + +#if NET6_0_OR_GREATER + public override async ValueTask DisposeAsync() + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + byte[]? buffer = _buffer; + _buffer = null; + if (buffer is not null) + { + ArrayPool.Shared.Return(buffer); + } + + if (!_leaveOpen) + { + await _stream.DisposeAsync().ConfigureAwait(false); + } + } +#endif + + public override void Flush() { } + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + private void ThrowIfDisposed() + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(BufferedRemainderStream)); + } + } + + private static void ValidateArguments(byte[] buffer, int offset, int count) + { + if (buffer is null) throw new ArgumentNullException(nameof(buffer)); + if (offset < 0) throw new ArgumentOutOfRangeException(nameof(offset)); + if (count < 0) throw new ArgumentOutOfRangeException(nameof(count)); + if (buffer.Length - offset < count) throw new ArgumentException("Offset and count exceed the buffer length."); + } +} + +internal sealed class BoundedReadStream : Stream +{ + private readonly Stream _stream; + private readonly long _length; + private long _position; + + public long Remaining => _length - _position; + + public BoundedReadStream(Stream stream, long length) + { + _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + _length = length >= 0 ? length : throw new ArgumentOutOfRangeException(nameof(length)); + } + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => _length; + + public override long Position + { + get => _position; + set => throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + int requested = (int)Math.Min(count, Remaining); + int read = _stream.Read(buffer, offset, requested); + _position += read; + return read; + } + +#if NET6_0_OR_GREATER + public override int Read(Span buffer) + { + int requested = (int)Math.Min(buffer.Length, Remaining); + int read = _stream.Read(buffer[..requested]); + _position += read; + return read; + } + + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default) + { + int requested = (int)Math.Min(buffer.Length, Remaining); + int read = await _stream.ReadAsync(buffer[..requested], cancellationToken).ConfigureAwait(false); + _position += read; + return read; + } +#endif + + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken) + { + int requested = (int)Math.Min(count, Remaining); + int read = await _stream.ReadAsync(buffer, offset, requested, cancellationToken).ConfigureAwait(false); + _position += read; + return read; + } + + public override void Flush() { } + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); +} + +internal sealed class DecompressionTransitionStream : Stream +{ + private readonly Stream _decompressedStream; + private readonly BoundedReadStream _compressedStream; + private readonly BufferedRemainderStream _remainderStream; + + private long _decompressedRemaining; + private bool _isDecoding = true; + private bool _isDisposed; + + public DecompressionTransitionStream( + Stream decompressedStream, + BoundedReadStream compressedStream, + BufferedRemainderStream remainderStream, + long decompressedLength) + { + _decompressedStream = decompressedStream ?? throw new ArgumentNullException(nameof(decompressedStream)); + _compressedStream = compressedStream ?? throw new ArgumentNullException(nameof(compressedStream)); + _remainderStream = remainderStream ?? throw new ArgumentNullException(nameof(remainderStream)); + _decompressedRemaining = decompressedLength >= 0 + ? decompressedLength + : throw new ArgumentOutOfRangeException(nameof(decompressedLength)); + } + + public override bool CanRead => !_isDisposed; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => throw new NotSupportedException(); + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + ThrowIfDisposed(); + if (count == 0) return 0; + + if (_isDecoding) + { + if (_decompressedRemaining != 0) + { + int requested = (int)Math.Min(count, _decompressedRemaining); + int read = _decompressedStream.Read(buffer, offset, requested); + if (read == 0) ThrowUnexpectedDecompressedEof(); + + _decompressedRemaining -= read; + return read; + } + + FinishDecoding(); + } + + return _remainderStream.Read(buffer, offset, count); + } + +#if NET6_0_OR_GREATER + public override int Read(Span buffer) + { + ThrowIfDisposed(); + if (buffer.IsEmpty) return 0; + + if (_isDecoding) + { + if (_decompressedRemaining != 0) + { + int requested = (int)Math.Min(buffer.Length, _decompressedRemaining); + int read = _decompressedStream.Read(buffer[..requested]); + if (read == 0) ThrowUnexpectedDecompressedEof(); + + _decompressedRemaining -= read; + return read; + } + + FinishDecoding(); + } + + return _remainderStream.Read(buffer); + } + + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + if (buffer.IsEmpty) return 0; + + if (_isDecoding) + { + if (_decompressedRemaining != 0) + { + int requested = (int)Math.Min(buffer.Length, _decompressedRemaining); + int read = await _decompressedStream.ReadAsync(buffer[..requested], cancellationToken).ConfigureAwait(false); + if (read == 0) ThrowUnexpectedDecompressedEof(); + + _decompressedRemaining -= read; + return read; + } + + await FinishDecodingAsync(cancellationToken).ConfigureAwait(false); + } + + return await _remainderStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + } +#endif + + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken) + { + ThrowIfDisposed(); + if (count == 0) return 0; + + if (_isDecoding) + { + if (_decompressedRemaining != 0) + { + int requested = (int)Math.Min(count, _decompressedRemaining); + int read = await _decompressedStream.ReadAsync(buffer, offset, requested, cancellationToken).ConfigureAwait(false); + if (read == 0) ThrowUnexpectedDecompressedEof(); + + _decompressedRemaining -= read; + return read; + } + + await FinishDecodingAsync(cancellationToken).ConfigureAwait(false); + } + + return await _remainderStream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + } + + private void FinishDecoding() + { + if (_decompressedStream.ReadByte() != -1) + { + throw new InvalidDataException("The decompressed segment is longer than its declared length."); + } + + byte[] scratch = ArrayPool.Shared.Rent(4096); + try + { + while (_compressedStream.Remaining != 0) + { + int read = _compressedStream.Read(scratch, 0, (int)Math.Min(scratch.Length, _compressedStream.Remaining)); + if (read == 0) throw new EndOfStreamException("The compressed segment ended before its declared length."); + } + } + finally + { + ArrayPool.Shared.Return(scratch); + } + + _decompressedStream.Dispose(); + _isDecoding = false; + } + + private async ValueTask FinishDecodingAsync(CancellationToken token) + { + byte[] scratch = ArrayPool.Shared.Rent(4096); + try + { + int extra = await _decompressedStream.ReadAsync(scratch, 0, 1, token).ConfigureAwait(false); + if (extra != 0) + { + throw new InvalidDataException("The decompressed segment is longer than its declared length."); + } + + while (_compressedStream.Remaining != 0) + { + int read = await _compressedStream.ReadAsync( + scratch, + 0, + (int)Math.Min(scratch.Length, _compressedStream.Remaining), + token).ConfigureAwait(false); + + if (read == 0) throw new EndOfStreamException("The compressed segment ended before its declared length."); + } + } + finally + { + ArrayPool.Shared.Return(scratch); + } + +#if NET6_0_OR_GREATER + await _decompressedStream.DisposeAsync().ConfigureAwait(false); +#else + _decompressedStream.Dispose(); +#endif + _isDecoding = false; + } + + protected override void Dispose(bool disposing) + { + if (_isDisposed) return; + _isDisposed = true; + + if (disposing) + { + _decompressedStream.Dispose(); + _compressedStream.Dispose(); + _remainderStream.Dispose(); + } + + base.Dispose(disposing); + } + +#if NET6_0_OR_GREATER + public override async ValueTask DisposeAsync() + { + if (_isDisposed) return; + _isDisposed = true; + + await _decompressedStream.DisposeAsync().ConfigureAwait(false); + _compressedStream.Dispose(); + await _remainderStream.DisposeAsync().ConfigureAwait(false); + GC.SuppressFinalize(this); + } +#endif + + public override void Flush() { } + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + private void ThrowIfDisposed() + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(DecompressionTransitionStream)); + } + } + + private static void ThrowUnexpectedDecompressedEof() + => throw new EndOfStreamException("The decompressed segment ended before its declared length."); +} diff --git a/SharpHPatchZ/IO/Reader/PrefetchedReadStream.cs b/SharpHPatchZ/IO/Reader/PrefetchedReadStream.cs new file mode 100644 index 0000000..6806849 --- /dev/null +++ b/SharpHPatchZ/IO/Reader/PrefetchedReadStream.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Runtime.ExceptionServices; +using System.Threading; +using System.Threading.Tasks; +using SharpHPatchZ.Extension; + +namespace SharpHPatchZ.IO.Reader; + +internal sealed class PrefetchedReadStream : Stream +{ + private readonly Stream _source; + private readonly int _bufferSize; + private readonly BlockingCollection _queue; + private readonly CancellationTokenSource _disposeCancellation = new(); + private readonly Task _producer; + + private ExceptionDispatchInfo? _producerFailure; + private BufferChunk? _current; + private int _currentOffset; + private int _disposed; + + internal PrefetchedReadStream(Stream source, + int bufferSize, + int queueCapacity = 2) + { + _source = source ?? throw new ArgumentNullException(nameof(source)); + if (!source.CanRead) + { + throw new ArgumentException("The source stream must be readable.", nameof(source)); + } + + _bufferSize = bufferSize > 0 + ? bufferSize + : throw new ArgumentOutOfRangeException(nameof(bufferSize)); + _queue = new BlockingCollection(queueCapacity > 0 + ? queueCapacity + : throw new ArgumentOutOfRangeException(nameof(queueCapacity))); + + _producer = Task.Factory.StartNew(Produce, + CancellationToken.None, + TaskCreationOptions.LongRunning, + TaskScheduler.Default); + } + + public override bool CanRead => Volatile.Read(ref _disposed) == 0; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => throw new NotSupportedException(); + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (buffer is null) throw new ArgumentNullException(nameof(buffer)); + if (offset < 0) throw new ArgumentOutOfRangeException(nameof(offset)); + if (count < 0 || buffer.Length - offset < count) throw new ArgumentOutOfRangeException(nameof(count)); + + return ReadCore(buffer.AsSpan(offset, count)); + } + +#if NET6_0_OR_GREATER + public override int Read(Span buffer) + => ReadCore(buffer); +#endif + + private int ReadCore(Span destination) + { + ThrowIfDisposed(); + int totalRead = 0; + + while (!destination.IsEmpty) + { + if (_current is null) + { + try + { + if (!_queue.TryTake(out BufferChunk? chunk, + Timeout.Infinite, + _disposeCancellation.Token)) + { + _producerFailure?.Throw(); + break; + } + + _current = chunk; + _currentOffset = 0; + } + catch (OperationCanceledException) when (Volatile.Read(ref _disposed) != 0) + { + throw new ObjectDisposedException(nameof(PrefetchedReadStream)); + } + } + + BufferChunk current = _current!; + int step = Math.Min(destination.Length, current.Length - _currentOffset); + current.Buffer.Span.Slice(_currentOffset, step).CopyTo(destination); + _currentOffset += step; + totalRead += step; + destination = destination[step..]; + + if (_currentOffset != current.Length) + { + continue; + } + + current.Dispose(); + _current = null; + _currentOffset = 0; + } + + return totalRead; + } + + private void Produce() + { +#if !NET6_0_OR_GREATER + byte[] streamBuffer = BigArrayPool.Shared.Rent(_bufferSize); +#endif + try + { + while (!_disposeCancellation.IsCancellationRequested) + { + NativeMemoryBuffer buffer = new(_bufferSize); + try + { +#if NET6_0_OR_GREATER + int read = _source.Read(buffer.Span); +#else + int read = _source.Read(streamBuffer, 0, streamBuffer.Length); + streamBuffer.AsSpan(0, read).CopyTo(buffer.Span); +#endif + if (read == 0) + { + buffer.Dispose(); + break; + } + + _queue.Add(new BufferChunk(buffer, read), _disposeCancellation.Token); + } + catch + { + buffer.Dispose(); + throw; + } + } + } + catch (OperationCanceledException) when (_disposeCancellation.IsCancellationRequested) { } + catch (ObjectDisposedException) when (_disposeCancellation.IsCancellationRequested) { } + catch (Exception exception) + { + Interlocked.CompareExchange(ref _producerFailure, + ExceptionDispatchInfo.Capture(exception), + null); + } + finally + { +#if !NET6_0_OR_GREATER + BigArrayPool.Shared.Return(streamBuffer); +#endif + try + { + _source.Dispose(); + } + catch (Exception exception) + { + if (!_disposeCancellation.IsCancellationRequested) + { + Interlocked.CompareExchange(ref _producerFailure, + ExceptionDispatchInfo.Capture(exception), + null); + } + } + finally + { + _queue.CompleteAdding(); + } + } + } + + protected override void Dispose(bool disposing) + { + if (Interlocked.Exchange(ref _disposed, 1) != 0) + { + return; + } + + if (disposing) + { + _disposeCancellation.Cancel(); + // Produce owns _source and disposes it after its final read. + _producer.GetAwaiter().GetResult(); + + _current?.Dispose(); + _current = null; + while (_queue.TryTake(out BufferChunk? chunk)) + { + chunk.Dispose(); + } + + _queue.Dispose(); + _disposeCancellation.Dispose(); + } + + base.Dispose(disposing); + } + + public override void Flush() { } + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + private void ThrowIfDisposed() + { + if (Volatile.Read(ref _disposed) != 0) + { + throw new ObjectDisposedException(nameof(PrefetchedReadStream)); + } + } + + private sealed class BufferChunk(NativeMemoryBuffer buffer, int length) : IDisposable + { + public NativeMemoryBuffer Buffer { get; } = buffer; + public int Length { get; } = length; + + public void Dispose() + => Buffer.Dispose(); + } +} diff --git a/SharpHPatchZ/IO/Reader/RandomMergedStreamWrapper.cs b/SharpHPatchZ/IO/Reader/RandomMergedStreamWrapper.cs new file mode 100644 index 0000000..48b6435 --- /dev/null +++ b/SharpHPatchZ/IO/Reader/RandomMergedStreamWrapper.cs @@ -0,0 +1,245 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Threading; + +namespace SharpHPatchZ.IO.Reader; + +internal sealed class RandomMergedStreamWrapper : IDisposable +{ + private readonly ConcurrentDictionary> _fileStreams = []; + private readonly ReaderWriterLockSlim _lifetimeLock = new(); + private readonly long[] _fileStreamEnds; + private readonly string[] _fileStreamPaths; + private readonly FileAccess _fileAccess; + private int _disposed; + + public long Length => _fileStreamEnds.Length == 0 ? 0 : _fileStreamEnds[^1]; + + internal RandomMergedStreamWrapper(string[] fileStreams, + long[] fileStreamEnds, + bool createFiles = false) + { + if (fileStreams is null) + { + throw new ArgumentNullException(nameof(fileStreams)); + } + + if (fileStreamEnds is null) + { + throw new ArgumentNullException(nameof(fileStreamEnds)); + } + + if (fileStreams.Length != fileStreamEnds.Length) + { + throw new ArgumentException("The path and end-offset arrays must have the same length.", + nameof(fileStreamEnds)); + } + + long previousEnd = 0; + for (int index = 0; index < fileStreams.Length; index++) + { + if (string.IsNullOrWhiteSpace(fileStreams[index])) + { + throw new ArgumentException("A file path cannot be null or empty.", nameof(fileStreams)); + } + + if (fileStreamEnds[index] < previousEnd) + { + throw new ArgumentException("File end offsets must be non-negative and ordered.", + nameof(fileStreamEnds)); + } + + previousEnd = fileStreamEnds[index]; + } + + _fileStreamPaths = fileStreams; + _fileStreamEnds = fileStreamEnds; + _fileAccess = createFiles ? FileAccess.ReadWrite : FileAccess.Read; + + if (createFiles) + { + CreateOutputFiles(); + } + } + + public void Write(Span buffer, long offset) + { + _lifetimeLock.EnterReadLock(); + try + { + ThrowIfDisposed(); + ValidateOffset(offset); + + if (buffer.Length > Length - offset) + { + throw new EndOfStreamException("The write exceeds the merged stream length."); + } + + while (!buffer.IsEmpty) + { + int streamIndex = FindStreamIndex(offset); + long streamStart = GetStreamStart(streamIndex); + int writeLength = (int)Math.Min(buffer.Length, _fileStreamEnds[streamIndex] - offset); + + FileStream stream = GetFileStream(streamIndex); + RandomAccessCompat.Write(stream.SafeFileHandle, + buffer[..writeLength], + offset - streamStart); + + buffer = buffer[writeLength..]; + offset += writeLength; + } + } + finally + { + _lifetimeLock.ExitReadLock(); + } + } + + public int Read(Span buffer, long offset) + { + _lifetimeLock.EnterReadLock(); + try + { + ThrowIfDisposed(); + ValidateOffset(offset); + + int totalRead = 0; + while (!buffer.IsEmpty && offset < Length) + { + int streamIndex = FindStreamIndex(offset); + long streamStart = GetStreamStart(streamIndex); + int readLength = (int)Math.Min(buffer.Length, _fileStreamEnds[streamIndex] - offset); + + FileStream stream = GetFileStream(streamIndex); + int read = RandomAccessCompat.Read(stream.SafeFileHandle, + buffer[..readLength], + offset - streamStart); + + if (read == 0) + { + throw new EndOfStreamException("An underlying file ended before its declared length."); + } + + totalRead += read; + offset += read; + buffer = buffer[read..]; + } + + return totalRead; + } + finally + { + _lifetimeLock.ExitReadLock(); + } + } + + public void Dispose() + { + if (Interlocked.Exchange(ref _disposed, 1) == 1) + { + return; + } + + _lifetimeLock.EnterWriteLock(); + try + { + foreach (KeyValuePair> kvp in _fileStreams) + { + if (kvp.Value.IsValueCreated) + { + kvp.Value.Value.Dispose(); + } + } + _fileStreams.Clear(); + } + finally + { + _lifetimeLock.ExitWriteLock(); + _lifetimeLock.Dispose(); + } + } + + private FileStream GetFileStream(int streamIndex) + { + Lazy lazyStream = _fileStreams.GetOrAdd( + streamIndex, + index => new Lazy( + () => new FileStream(_fileStreamPaths[index], + FileMode.Open, + _fileAccess, + FileShare.ReadWrite, + bufferSize: 1, + FileOptions.RandomAccess), + LazyThreadSafetyMode.ExecutionAndPublication)); + + return lazyStream.Value; + } + + private void CreateOutputFiles() + { + long streamStart = 0; + for (int index = 0; index < _fileStreamPaths.Length; index++) + { + string path = _fileStreamPaths[index]; + string? directoryPath = Path.GetDirectoryName(path); + if (!string.IsNullOrEmpty(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + + using FileStream stream = new(path, + FileMode.Create, + FileAccess.ReadWrite, + FileShare.ReadWrite, + bufferSize: 1, + FileOptions.RandomAccess); + stream.SetLength(_fileStreamEnds[index] - streamStart); + streamStart = _fileStreamEnds[index]; + } + } + + private int FindStreamIndex(long offset) + { + int low = 0; + int high = _fileStreamEnds.Length - 1; + + // Find the first segment whose cumulative end is greater than the offset. + // This also skips any zero-length segments. + while (low < high) + { + int middle = low + ((high - low) >> 1); + if (_fileStreamEnds[middle] > offset) + { + high = middle; + } + else + { + low = middle + 1; + } + } + + return low; + } + + private long GetStreamStart(int streamIndex) + => streamIndex == 0 ? 0 : _fileStreamEnds[streamIndex - 1]; + + private void ValidateOffset(long offset) + { + if ((ulong)offset > (ulong)Length) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + } + + private void ThrowIfDisposed() + { + if (Volatile.Read(ref _disposed) != 0) + { + throw new ObjectDisposedException(nameof(RandomMergedStreamWrapper)); + } + } +} diff --git a/SharpHPatchZ/InitializeOptions.cs b/SharpHPatchZ/InitializeOptions.cs new file mode 100644 index 0000000..a257d48 --- /dev/null +++ b/SharpHPatchZ/InitializeOptions.cs @@ -0,0 +1,23 @@ +using System.Runtime.InteropServices; +// ReSharper disable IdentifierTypo +// ReSharper disable CommentTypo + +namespace SharpHPatchZ; + +/// +/// Specifies the options during Patch context initialization. +/// +[StructLayout(LayoutKind.Sequential)] +public struct InitializeOptions() +{ + private int _isKuroGamesHDiff = 0; + + /// + /// Specifies whether the patch file is a Kuro Games DirHDiff format. + /// + public bool IsKuroGamesHDiff + { + get => _isKuroGamesHDiff > 0; + set => _isKuroGamesHDiff = value ? 1 : 0; + } +} diff --git a/SharpHPatchZ/Native/PInvoke.Unix.cs b/SharpHPatchZ/Native/PInvoke.Unix.cs new file mode 100644 index 0000000..bfd6369 --- /dev/null +++ b/SharpHPatchZ/Native/PInvoke.Unix.cs @@ -0,0 +1,41 @@ +using System.Runtime.InteropServices; +// ReSharper disable IdentifierTypo + +namespace SharpHPatchZ.Native; + +internal static unsafe partial class PInvoke +{ + public static class Unix + { + [DllImport("libc", EntryPoint = "fileno", CallingConvention = CallingConvention.Cdecl)] + public static extern int GetFileDescriptor(void* stream); + + [DllImport("libc", EntryPoint = "pread", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern nint PRead( + int fileDescriptor, + ref byte buffer, + nuint count, + long fileOffset); + + [DllImport("libc", EntryPoint = "pread64", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern nint PRead64( + int fileDescriptor, + ref byte buffer, + nuint count, + long fileOffset); + + [DllImport("libc", EntryPoint = "pwrite", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern nint PWrite( + int fileDescriptor, + ref byte buffer, + nuint count, + long fileOffset); + + [DllImport("libc", EntryPoint = "pwrite64", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern nint PWrite64( + int fileDescriptor, + ref byte buffer, + nuint count, + long fileOffset); + } +} diff --git a/SharpHPatchZ/Native/PInvoke.Windows.cs b/SharpHPatchZ/Native/PInvoke.Windows.cs new file mode 100644 index 0000000..93fa91d --- /dev/null +++ b/SharpHPatchZ/Native/PInvoke.Windows.cs @@ -0,0 +1,44 @@ +using System.Runtime.InteropServices; +// ReSharper disable StringLiteralTypo + +namespace SharpHPatchZ.Native; + +internal static unsafe partial class PInvoke +{ + public static class Windows + { + [DllImport("ucrtbase", EntryPoint = "_fileno", CallingConvention = CallingConvention.Cdecl)] + public static extern int GetFileDescriptor(void* stream); + + [DllImport("ucrtbase", EntryPoint = "_get_osfhandle", CallingConvention = CallingConvention.Cdecl)] + public static extern nint GetOSFileHandle(int fd); + + [DllImport("kernel32", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool ReadFile( + nint fileHandle, + ref byte buffer, + uint bytesToRead, + out uint bytesRead, + ref NativeOverlappedData overlapped); + + [DllImport("kernel32", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool WriteFile( + nint fileHandle, + ref byte buffer, + uint bytesToWrite, + out uint bytesWritten, + ref NativeOverlappedData overlapped); + + [StructLayout(LayoutKind.Sequential)] + public struct NativeOverlappedData(long offset) + { + private readonly nint _internal = 0; + private readonly nint _internalHigh = 0; + private readonly uint _offset = unchecked((uint)offset); + private readonly uint _offsetHigh = unchecked((uint)(offset >> 32)); + private readonly nint _eventHandle = 0; + } + } +} diff --git a/SharpHPatchZ/Native/PInvoke.cs b/SharpHPatchZ/Native/PInvoke.cs new file mode 100644 index 0000000..9862021 --- /dev/null +++ b/SharpHPatchZ/Native/PInvoke.cs @@ -0,0 +1,37 @@ +using System; +using System.IO; +using Microsoft.Win32.SafeHandles; +// ReSharper disable InconsistentNaming + +namespace SharpHPatchZ.Native; + +internal static unsafe partial class PInvoke +{ +#if NET8_0_OR_GREATER + public static SafeFileHandle GetSafeFileHandleFromFILE(void* file) + { + nint handle; + + if (OperatingSystem.IsWindows()) + { + int fd = Windows.GetFileDescriptor(file); + if (fd < 0) + throw new IOException("_fileno() failed."); + + handle = Windows.GetOSFileHandle(fd); + if (handle == -1) + throw new IOException("_get_osfhandle() failed."); + } + else + { + int fd = Unix.GetFileDescriptor(file); + if (fd < 0) + throw new IOException("fileno() failed."); + + handle = fd; + } + + return new SafeFileHandle(handle, ownsHandle: false); + } +#endif +} diff --git a/SharpHPatchZ/NetStandardCompat/System.Runtime.CompilerServices/CompilerFeatureRequiredAttribute.cs b/SharpHPatchZ/NetStandardCompat/System.Runtime.CompilerServices/CompilerFeatureRequiredAttribute.cs new file mode 100644 index 0000000..7191d28 --- /dev/null +++ b/SharpHPatchZ/NetStandardCompat/System.Runtime.CompilerServices/CompilerFeatureRequiredAttribute.cs @@ -0,0 +1,28 @@ +#pragma warning disable IDE0130 +#if !NET7_0_OR_GREATER +namespace System.Runtime.CompilerServices; + +[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] +public sealed class CompilerFeatureRequiredAttribute(string featureName) : Attribute +{ + /// + /// The name of the compiler feature. + /// + public string FeatureName { get; } = featureName; + + /// + /// If true, the compiler can choose to allow access to the location where this attribute is applied if it does not understand . + /// + public bool IsOptional { get; init; } + + /// + /// The used for the ref structs C# feature. + /// + public const string RefStructs = nameof(RefStructs); + + /// + /// The used for the required members C# feature. + /// + public const string RequiredMembers = nameof(RequiredMembers); +} +#endif \ No newline at end of file diff --git a/SharpHPatchZ/NetStandardCompat/System.Runtime.CompilerServices/IsExternalInit.cs b/SharpHPatchZ/NetStandardCompat/System.Runtime.CompilerServices/IsExternalInit.cs new file mode 100644 index 0000000..f32817a --- /dev/null +++ b/SharpHPatchZ/NetStandardCompat/System.Runtime.CompilerServices/IsExternalInit.cs @@ -0,0 +1,6 @@ +#pragma warning disable IDE0130 +#if NETSTANDARD2_0 +namespace System.Runtime.CompilerServices; + +internal static class IsExternalInit; +#endif \ No newline at end of file diff --git a/SharpHPatchZ/NetStandardCompat/System.Runtime.CompilerServices/RequiredMemberAttribute.cs b/SharpHPatchZ/NetStandardCompat/System.Runtime.CompilerServices/RequiredMemberAttribute.cs new file mode 100644 index 0000000..230b71b --- /dev/null +++ b/SharpHPatchZ/NetStandardCompat/System.Runtime.CompilerServices/RequiredMemberAttribute.cs @@ -0,0 +1,9 @@ +#pragma warning disable IDE0130 +#if !NET7_0_OR_GREATER +namespace System.Runtime.CompilerServices; + +/// Specifies that a type has required members or that a member is required. +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, Inherited = false)] +[ComponentModel.EditorBrowsable(ComponentModel.EditorBrowsableState.Never)] +internal sealed class RequiredMemberAttribute : Attribute; +#endif diff --git a/SharpHDiffPatch.Core/Pack.cmd b/SharpHPatchZ/Pack.cmd similarity index 70% rename from SharpHDiffPatch.Core/Pack.cmd rename to SharpHPatchZ/Pack.cmd index 1841033..8dc680c 100644 --- a/SharpHDiffPatch.Core/Pack.cmd +++ b/SharpHPatchZ/Pack.cmd @@ -1,10 +1,10 @@ @echo off if /i not exist "artifacts" mkdir "artifacts" dotnet restore || goto :Fail -dotnet clean -c Release SharpHDiffPatch.Core.csproj || goto :Fail +dotnet clean -c Release SharpHPatchZ.csproj || goto :Fail call :Clean -dotnet build -c Release SharpHDiffPatch.Core.csproj || goto :Fail -dotnet pack -c Release -o artifacts -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg SharpHDiffPatch.Core.csproj || goto :Fail +dotnet build -c Release SharpHPatchZ.csproj || goto :Fail +dotnet pack -c Release -o artifacts -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg SharpHPatchZ.csproj || goto :Fail goto :Success :Fail diff --git a/SharpHPatchZ/Patch/HDiff13DerivedPatcher.CopySimilarFilesContext.cs b/SharpHPatchZ/Patch/HDiff13DerivedPatcher.CopySimilarFilesContext.cs new file mode 100644 index 0000000..b37f966 --- /dev/null +++ b/SharpHPatchZ/Patch/HDiff13DerivedPatcher.CopySimilarFilesContext.cs @@ -0,0 +1,99 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpHPatchZ.Extension; + +namespace SharpHPatchZ.Patch; + +internal sealed partial class HDiff13DerivedPatcher +{ + private class CopySimilarFilesContext(string inputDir, + string[] inputPaths, + string outputDir, + string[] outputPaths) + { + private string InputDir { get; } = inputDir; + private string OutputDir { get; } = outputDir; + + private string[] InputPaths { get; } = inputPaths; + private string[] OutputPaths { get; } = outputPaths; + + public void RunCopy(PatcherBase patcher, + PatchOptions options, + CancellationToken token) + { + int bufferSize = options.CopyBufferSize; + if (bufferSize <= 0) bufferSize = 16 << 10; + + token.ThrowIfCancellationRequested(); + int count = InputPaths.Length; + + Parallel.For(0, count, new ParallelOptions + { + MaxDegreeOfParallelism = (int)options.ParallelThreads + }, i => + { + token.ThrowIfCancellationRequested(); + +#if NET6_0_OR_GREATER + unsafe + { + void* bufferP = MemoryAlloc.Alloc(bufferSize); + var buffer = new Span(bufferP, bufferSize); + try + { + string inputPath = Path.GetFullPath(Path.Combine(InputDir, InputPaths[i])); + string outputPath = Path.GetFullPath(Path.Combine(OutputDir, OutputPaths[i])); + + if (Path.GetDirectoryName(outputPath) is { } outputDir) + Directory.CreateDirectory(outputDir); + + using FileStream inputStream = File.Open(inputPath, FileMode.Open, FileAccess.Read); + using FileStream outputStream = File.Create(outputPath, bufferSize); + int read; + + while ((read = inputStream.Read(buffer)) > 0) + { + token.ThrowIfCancellationRequested(); + + outputStream.Write(buffer[..read]); + patcher.AdvanceProgress(read); + } + } + finally + { + MemoryAlloc.FreeRaw(bufferP); + } + } +#else + byte[] buffer = BigArrayPool.Shared.Rent(bufferSize); + try + { + string inputPath = Path.GetFullPath(Path.Combine(InputDir, InputPaths[i])); + string outputPath = Path.GetFullPath(Path.Combine(OutputDir, OutputPaths[i])); + + if (Path.GetDirectoryName(outputPath) is { } outputDir) + Directory.CreateDirectory(outputDir); + + using FileStream inputStream = File.Open(inputPath, FileMode.Open, FileAccess.Read); + using FileStream outputStream = File.Create(outputPath, bufferSize); + int read; + + while ((read = inputStream.Read(buffer, 0, bufferSize)) > 0) + { + token.ThrowIfCancellationRequested(); + + outputStream.Write(buffer, 0, read); + patcher.AdvanceProgress(read); + } + } + finally + { + BigArrayPool.Shared.Return(buffer); + } +#endif + }); + } + } +} diff --git a/SharpHPatchZ/Patch/HDiff13DerivedPatcher.CorePatcher.cs b/SharpHPatchZ/Patch/HDiff13DerivedPatcher.CorePatcher.cs new file mode 100644 index 0000000..440d7e2 --- /dev/null +++ b/SharpHPatchZ/Patch/HDiff13DerivedPatcher.CorePatcher.cs @@ -0,0 +1,665 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +#if NET6_0_OR_GREATER +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +#endif +using System.Runtime.ExceptionServices; +using System.Threading; +using System.Threading.Tasks; +using SharpHPatchZ.Extension; +using SharpHPatchZ.Header.Metadata; +using SharpHPatchZ.IO.Reader; +// ReSharper disable AccessToDisposedClosure + +namespace SharpHPatchZ.Patch; + +internal sealed partial class HDiff13DerivedPatcher +{ + private const int DefaultPatchBufferSize = 1 << 20; + + private unsafe void StartCorePatcher(CancellationToken token) + { + using NativeMemoryBuffer rleCoverBuffer = RleCoverInfo.Read(_coverReader, + Info, + token); + ReadOnlySpan rleCoverSpan = rleCoverBuffer.Span; + if (InputStream is null || OutputStream is null) + { + throw new InvalidOperationException("The patch input and output streams have not been initialized."); + } + + PatchMetadata patchMetadata = Info.GetPatchMetadata(); + ValidateReaderOffset(_coverReader, + patchMetadata.CoverDataSizeP->Size, + "cover"); + + int bufferSize = Options.PatchWorkerBufferSize > 0 + ? Options.PatchWorkerBufferSize + : DefaultPatchBufferSize; + int workerCount = GetWorkerCount(Options.ParallelThreads); + + if (workerCount == 1) + { + RunSequential(rleCoverSpan, patchMetadata, bufferSize, token); + } + else + { + RunParallel(rleCoverBuffer, + patchMetadata, + bufferSize, + workerCount, + token); + } + } + + private static int GetWorkerCount(uint requestedWorkerCount) + { + return requestedWorkerCount switch + { + 0 => Math.Max(1, Environment.ProcessorCount), + > int.MaxValue => throw new ArgumentOutOfRangeException(nameof(PatchOptions.ParallelThreads)), + _ => (int)requestedWorkerCount + }; + } + + private void RunSequential(ReadOnlySpan covers, + PatchMetadata patchMetadata, + int bufferSize, + CancellationToken token) + { + using NativeMemoryBuffer oldBuffer = new(bufferSize); + ProduceWork(covers, + patchMetadata.DiffNewSize, + bufferSize, + item => ProcessWorkItem(item, oldBuffer, token), + token); + ValidateDataReaders(patchMetadata); + } + + private void RunParallel(NativeMemoryBuffer covers, + PatchMetadata patchMetadata, + int bufferSize, + int workerCount, + CancellationToken token) + { + int queueCapacity = workerCount > int.MaxValue / 2 + ? int.MaxValue + : workerCount * 2; + + using CancellationTokenSource linkedCancellation = + CancellationTokenSource.CreateLinkedTokenSource(token); + using BlockingCollection workQueue = new(queueCapacity); + + ExceptionDispatchInfo? producerFailure = null; + ExceptionDispatchInfo? workerFailure = null; + ExceptionDispatchInfo? parallelFailure = null; + + Task producer = Task.Factory.StartNew( + () => + { + try + { + ProduceWork(covers.Span, + patchMetadata.DiffNewSize, + bufferSize, + item => AddWorkItem(workQueue, item, linkedCancellation.Token), + linkedCancellation.Token); + ValidateDataReaders(patchMetadata); + } + catch (Exception exception) + { + producerFailure = ExceptionDispatchInfo.Capture(exception); + linkedCancellation.Cancel(); + } + finally + { + workQueue.CompleteAdding(); + } + }, + CancellationToken.None, + TaskCreationOptions.LongRunning, + TaskScheduler.Default); + + try + { + Parallel.ForEach( + workQueue.GetConsumingEnumerable(linkedCancellation.Token), + new ParallelOptions + { + CancellationToken = linkedCancellation.Token, + MaxDegreeOfParallelism = workerCount + }, + () => new NativeMemoryBuffer(bufferSize), + (item, _, oldBuffer) => + { + try + { + ProcessWorkItem(item, oldBuffer, linkedCancellation.Token); + } + catch (Exception exception) + { + Interlocked.CompareExchange( + ref workerFailure, + ExceptionDispatchInfo.Capture(exception), + null); + linkedCancellation.Cancel(); + throw; + } + + return oldBuffer; + }, + oldBuffer => oldBuffer.Dispose()); + } + catch (Exception exception) + { + parallelFailure = ExceptionDispatchInfo.Capture(exception); + linkedCancellation.Cancel(); + } + finally + { + producer.GetAwaiter().GetResult(); + + while (workQueue.TryTake(out PatchWorkItem abandonedItem)) + { + abandonedItem.Buffer.Dispose(); + } + } + + // A real producer error is more useful than the cancellation observed by + // workers after the producer stopped. Conversely, a worker error takes + // precedence when cancellation is all the producer observed. + if (producerFailure?.SourceException is not OperationCanceledException) + { + producerFailure?.Throw(); + } + + workerFailure?.Throw(); + producerFailure?.Throw(); + parallelFailure?.Throw(); + } + + private static void AddWorkItem(BlockingCollection queue, + PatchWorkItem item, + CancellationToken token) + { + try + { + queue.Add(item, token); + } + catch + { + item.Buffer.Dispose(); + throw; + } + } + + private void ProduceWork(ReadOnlySpan covers, + long newDataSize, + int bufferSize, + Action emit, + CancellationToken token) + { + RleDecoder rleDecoder = new(_rleCtrlReader, _rleCodeReader); + using NativeMemoryBuffer skipBuffer = new(Math.Min(bufferSize, 64 << 10)); + using PatchWorkBuilder workBuilder = new(bufferSize, emit); + + long newPosition = 0; + for (int coverIndex = 0; coverIndex < covers.Length; coverIndex++) + { + token.ThrowIfCancellationRequested(); + ref readonly RleCoverInfo cover = ref covers[coverIndex]; + ValidateCover(cover, newPosition, newDataSize, InputStream!.Length); + + ProduceNewData(cover.CopyLength, + ref newPosition, + rleDecoder, + skipBuffer.Span, + workBuilder, + token); + ProduceCover(cover, + ref newPosition, + rleDecoder, + workBuilder, + token); + } + + ProduceNewData(newDataSize - newPosition, + ref newPosition, + rleDecoder, + skipBuffer.Span, + workBuilder, + token); + + if (newPosition != newDataSize || rleDecoder.HasPendingData) + { + throw new InvalidDataException("The decoded patch data does not match the declared output size."); + } + + workBuilder.Flush(); + } + + private void ProduceNewData(long length, + ref long newPosition, + RleDecoder rleDecoder, + Span skipBuffer, + PatchWorkBuilder workBuilder, + CancellationToken token) + { + while (length > 0) + { + token.ThrowIfCancellationRequested(); + Span destination = workBuilder + .GetWritableSpan(newPosition, + length, + out int step); + _newDataReader.ReadBytes(destination); + rleDecoder.Skip(step, skipBuffer, token); + workBuilder.CommitNewData(step); + + newPosition += step; + length -= step; + } + } + + private static void ProduceCover(RleCoverInfo cover, + ref long newPosition, + RleDecoder rleDecoder, + PatchWorkBuilder workBuilder, + CancellationToken token) + { + long coverOffset = 0; + while (coverOffset < cover.RleLength) + { + token.ThrowIfCancellationRequested(); + Span destination = workBuilder.GetWritableSpan( + newPosition, + cover.RleLength - coverOffset, + out int step); + rleDecoder.Decode(destination, token); + workBuilder.CommitCover(step, cover.OldStreamPosition + coverOffset); + + newPosition += step; + coverOffset += step; + } + } + + private void ProcessWorkItem(PatchWorkItem item, + NativeMemoryBuffer oldBuffer, + CancellationToken token) + { + try + { + token.ThrowIfCancellationRequested(); + if (item.CoverSegments is { } coverSegments) + { + for (int segmentIndex = 0; segmentIndex < coverSegments.Count; segmentIndex++) + { + token.ThrowIfCancellationRequested(); + CoverSegment segment = coverSegments[segmentIndex]; + Span oldData = oldBuffer.Span[..segment.Length]; + int read = InputStream!.Read(oldData, segment.OldPosition); + if (read != segment.Length) + { + throw new EndOfStreamException("The old-data stream ended while processing a cover."); + } + +#if NET6_0_OR_GREATER + Span rleData = item.Buffer.Span.Slice(segment.BufferOffset, segment.Length); + AddRle(rleData, oldData, Options.UseSIMD); +#else + Span rleData = item.Buffer.Span.Slice(segment.BufferOffset, segment.Length); + AddRle(rleData, oldData); +#endif + } + } + + OutputStream!.Write(item.Buffer.Span[..item.Length], item.OutputPosition); + AdvanceProgress(item.Length); + } + finally + { + item.Buffer.Dispose(); + } + } + +#if NET6_0_OR_GREATER + private static void AddRle(Span destination, + ReadOnlySpan addend, + bool useSimd) +#else + private static void AddRle(Span destination, + ReadOnlySpan addend) +#endif + { + if (destination.Length != addend.Length) + { + throw new ArgumentException("The old-data and RLE buffers must have the same length."); + } + + int length = destination.Length; + int index = 0; +#if NET6_0_OR_GREATER + if (useSimd && Avx2.IsSupported) + { + const int vectorLength = 32; + int vectorEnd = length - length % vectorLength; + if (vectorEnd != 0) + { + ref byte destinationRef = ref destination[0]; + ref byte addendRef = ref Unsafe.AsRef(in addend[0]); + for (; index < vectorEnd; index += vectorLength) + { + var destinationVector = Unsafe.ReadUnaligned>(ref Unsafe.Add(ref destinationRef, index)); + var addendVector = Unsafe.ReadUnaligned>(ref Unsafe.Add(ref addendRef, index)); + Vector256 result = Avx2.Add(destinationVector, addendVector); + Unsafe.WriteUnaligned(ref Unsafe.Add(ref destinationRef, index), result); + } + } + } + else if (useSimd && Sse2.IsSupported) + { + const int vectorLength = 16; + int vectorEnd = length - length % vectorLength; + if (vectorEnd != 0) + { + ref byte destinationRef = ref destination[0]; + ref byte addendRef = ref Unsafe.AsRef(in addend[0]); + for (; index < vectorEnd; index += vectorLength) + { + var destinationVector = Unsafe.ReadUnaligned>(ref Unsafe.Add(ref destinationRef, index)); + var addendVector = Unsafe.ReadUnaligned>(ref Unsafe.Add(ref addendRef, index)); + Vector128 result = Sse2.Add(destinationVector, addendVector); + Unsafe.WriteUnaligned(ref Unsafe.Add(ref destinationRef, index), result); + } + } + } + else if (useSimd && Vector.IsHardwareAccelerated) + { + int vectorLength = Vector.Count; + int vectorEnd = length - length % vectorLength; + if (vectorEnd != 0) + { + ref byte destinationRef = ref destination[0]; + ref byte addendRef = ref Unsafe.AsRef(in addend[0]); + for (; index < vectorEnd; index += vectorLength) + { + var destinationVector = Unsafe.ReadUnaligned>(ref Unsafe.Add(ref destinationRef, index)); + var addendVector = Unsafe.ReadUnaligned>(ref Unsafe.Add(ref addendRef, index)); + Vector result = destinationVector + addendVector; + Unsafe.WriteUnaligned(ref Unsafe.Add(ref destinationRef, index), result); + } + } + } +#endif + + while (index < length) + { + destination[index] = unchecked((byte)(destination[index] + addend[index++])); + } + } + + private static void ValidateCover(RleCoverInfo cover, + long expectedNewPosition, + long newDataSize, + long oldDataSize) + { + if (cover.CopyLength < 0 || cover.RleLength < 0 || cover.OldStreamPosition < 0 || + cover.NewStreamPosition < expectedNewPosition || + cover.CopyLength != cover.NewStreamPosition - expectedNewPosition || + cover.NewStreamPosition > newDataSize || + cover.RleLength > newDataSize - cover.NewStreamPosition || + cover.OldStreamPosition > oldDataSize || + cover.RleLength > oldDataSize - cover.OldStreamPosition) + { + throw new InvalidDataException("A patch cover is outside the declared old or new data bounds."); + } + } + + private unsafe void ValidateDataReaders(PatchMetadata patchMetadata) + { + ValidateReaderOffset(_rleCtrlReader, patchMetadata.RleControlDataSizeP->Size, "RLE control"); + ValidateReaderOffset(_rleCodeReader, patchMetadata.RleCodeDataSizeP->Size, "RLE code"); + ValidateReaderOffset(_newDataReader, patchMetadata.NewDiffDataSizeP->Size, "new-data"); + } + + private static void ValidateReaderOffset(BittableStreamReader reader, + long expectedOffset, + string dataName) + { + if (reader.Offset != expectedOffset) + { + throw new InvalidDataException( + $"The consumed {dataName} size ({reader.Offset}) does not match its declared size ({expectedOffset})."); + } + } + + private readonly struct PatchWorkItem + { + internal PatchWorkItem(NativeMemoryBuffer buffer, + int length, + long outputPosition, + List? coverSegments) + { + Buffer = buffer; + Length = length; + OutputPosition = outputPosition; + CoverSegments = coverSegments; + } + + public readonly NativeMemoryBuffer Buffer; + public readonly int Length; + public readonly long OutputPosition; + public readonly List? CoverSegments; + } + + private readonly struct CoverSegment + { + internal CoverSegment(int bufferOffset, + int length, + long oldPosition) + { + BufferOffset = bufferOffset; + Length = length; + OldPosition = oldPosition; + } + + public readonly int BufferOffset; + public readonly int Length; + public readonly long OldPosition; + } + + private sealed class PatchWorkBuilder : IDisposable + { + private NativeMemoryBuffer? _buffer; + private int _length; + private long _outputPosition; + private List? _coverSegments; + private readonly int _bufferSize; + private readonly Action _emit; + + internal PatchWorkBuilder(int bufferSize, + Action emit) + { + _bufferSize = bufferSize; + _emit = emit; + } + + public Span GetWritableSpan(long outputPosition, + long requestedLength, + out int writableLength) + { + if (_buffer is not null && _length == _bufferSize) + { + Flush(); + } + + if (_buffer is null) + { + _buffer = new NativeMemoryBuffer(_bufferSize); + _outputPosition = outputPosition; + } + else if (outputPosition != _outputPosition + _length) + { + throw new InvalidOperationException("Patch batches must contain contiguous output ranges."); + } + + writableLength = (int)Math.Min(requestedLength, _bufferSize - _length); + return _buffer.Span.Slice(_length, writableLength); + } + + public void CommitNewData(int length) + => _length += length; + + public void CommitCover(int length, long oldPosition) + { + _coverSegments ??= []; + + int segmentCount = _coverSegments.Count; + if (segmentCount > 0) + { + CoverSegment previous = _coverSegments[segmentCount - 1]; + if (previous.BufferOffset + previous.Length == _length && + previous.OldPosition + previous.Length == oldPosition) + { + _coverSegments[segmentCount - 1] = new CoverSegment(previous.BufferOffset, + previous.Length + length, + previous.OldPosition); + _length += length; + return; + } + } + + _coverSegments.Add(new CoverSegment(_length, length, oldPosition)); + _length += length; + } + + public void Flush() + { + if (_buffer is null || _length == 0) + { + return; + } + + PatchWorkItem item = new(_buffer, + _length, + _outputPosition, + _coverSegments); + _buffer = null; + _length = 0; + _coverSegments = null; + _emit(item); + } + + public void Dispose() + { + if (_buffer is null) return; + + _buffer.Dispose(); + _buffer = null; + } + } + + private sealed class RleDecoder + { + private long _remaining; + private byte _type; + private byte _value; + + private readonly BittableStreamReader _controlReader; + private readonly BittableStreamReader _codeReader; + + internal RleDecoder(BittableStreamReader controlReader, + BittableStreamReader codeReader) + { + _controlReader = controlReader; + _codeReader = codeReader; + } + + public bool HasPendingData => _remaining != 0; + + public void Decode(Span destination, CancellationToken token) + { + while (!destination.IsEmpty) + { + token.ThrowIfCancellationRequested(); + EnsureRun(); + + int step = (int)Math.Min(_remaining, destination.Length); + Span target = destination[..step]; + switch (_type) + { + case 0: + target.Clear(); + break; + case 1: + target.Fill(byte.MaxValue); + break; + case 2: + target.Fill(_value); + break; + case 3: + _codeReader.ReadBytes(target); + break; + default: + throw new InvalidDataException("The RLE control stream contains an unknown run type."); + } + + _remaining -= step; + destination = destination[step..]; + } + } + + public void Skip(long length, Span scratchBuffer, CancellationToken token) + { + while (length > 0) + { + token.ThrowIfCancellationRequested(); + EnsureRun(); + + long step = Math.Min(_remaining, length); + if (_type == 3) + { + long rawRemaining = step; + while (rawRemaining > 0) + { + int readLength = (int)Math.Min(rawRemaining, scratchBuffer.Length); + _codeReader.ReadBytes(scratchBuffer[..readLength]); + rawRemaining -= readLength; + } + } + + _remaining -= step; + length -= step; + } + } + + private void EnsureRun() + { + if (_remaining != 0) + { + return; + } + + byte firstByte = _controlReader.ReadByte(); + _type = (byte)(firstByte >> (8 - BittableStreamReader.KByteRleType)); + + long encodedLength = _controlReader.ReadLong7Bit(BittableStreamReader.KByteRleType, + firstByte); + if (encodedLength == long.MaxValue) + { + throw new InvalidDataException("An RLE run length exceeds the supported range."); + } + + _remaining = encodedLength + 1; + if (_type == 2) + { + _value = _codeReader.ReadByte(); + } + } + } +} diff --git a/SharpHPatchZ/Patch/HDiff13DerivedPatcher.cs b/SharpHPatchZ/Patch/HDiff13DerivedPatcher.cs new file mode 100644 index 0000000..c835360 --- /dev/null +++ b/SharpHPatchZ/Patch/HDiff13DerivedPatcher.cs @@ -0,0 +1,268 @@ +using System; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using SharpHPatchZ.Extension; +using SharpHPatchZ.Header; +using SharpHPatchZ.Header.Metadata; +using SharpHPatchZ.IO.Reader; +// ReSharper disable CommentTypo + +namespace SharpHPatchZ.Patch; + +internal sealed partial class HDiff13DerivedPatcher : PatcherBase +{ + private readonly BittableStreamReader _coverReader; + private readonly BittableStreamReader _rleCtrlReader; + private readonly BittableStreamReader _rleCodeReader; + private readonly BittableStreamReader _newDataReader; + + internal HDiff13DerivedPatcher( + BittableStreamReader coverReader, + BittableStreamReader rleCtrlReader, + BittableStreamReader rleCodeReader, + BittableStreamReader newDataReader, + HDiffInfo info, + PatchOptions options, + ProgressCallback progressCallback) + : base(info, options, progressCallback) + { + _coverReader = coverReader; + _rleCtrlReader = rleCtrlReader; + _rleCodeReader = rleCodeReader; + _newDataReader = newDataReader; + } + + private CopySimilarFilesContext? _copySimilarFilesContext; + + public override void StartPatch(string inputPath, string outputPath, CancellationToken token) + { + InitializeInputOutputStream(inputPath, outputPath); + + // Start both CopyOver and CorePatch routine if context is not null. + if (_copySimilarFilesContext != null) + { + TaskCompletionSource copyOverTcs = new(null!, TaskCreationOptions.RunContinuationsAsynchronously); + TaskCompletionSource corePatcherTcs = new(null!, TaskCreationOptions.RunContinuationsAsynchronously); + + Thread copyOverThread = new(_ => + { + try + { + _copySimilarFilesContext.RunCopy(this, Options, token); + copyOverTcs.SetResult(true); + } + catch (OperationCanceledException) when (token.IsCancellationRequested) + { +#if NET6_0_OR_GREATER + copyOverTcs.SetCanceled(token); +#else + copyOverTcs.SetCanceled(); +#endif + } + catch (Exception ex) + { + copyOverTcs.SetException(ex); + } + }); + + Thread corePatcherThread = new(_ => + { + try + { + StartCorePatcher(token); + corePatcherTcs.SetResult(true); + } + catch (OperationCanceledException) when (token.IsCancellationRequested) + { +#if NET6_0_OR_GREATER + copyOverTcs.SetCanceled(token); +#else + copyOverTcs.SetCanceled(); +#endif + } + catch (Exception ex) + { + corePatcherTcs.SetException(ex); + } + }); + + copyOverThread.Start(); + corePatcherThread.Start(); + Task.WhenAll(copyOverTcs.Task, corePatcherTcs.Task).Wait(token); + return; + } + + // Otherwise, run the CorePatch routine only + StartCorePatcher(token); + } + + public override Task StartPatchAsync(string inputPath, string outputPath, CancellationToken token) + => Task.Factory.StartNew(state => StartPatch(inputPath, outputPath, (CancellationToken)state!), + token, + TaskCreationOptions.LongRunning); + + private unsafe void InitializeInputOutputStream(string inputPath, string outputPath) + { + ref PatchMetadata patchMetadata = ref Info.GetPatchMetadata(); + if (Unsafe.IsNullRef(ref patchMetadata)) throw ExceptionHelper.ThrowHDiffInfoPatchMetadataNotAllocated(); + + // Return only single stream for HDiff13 + if (Info.MagicType == HDiffMagic.HDiff13) + { + FileInfo inputFile = inputPath.GetFileInfo(); + FileInfo outputFile = outputPath.GetFileInfo(); + + // Sanity input existence and its size + if (!inputFile.Exists) throw ExceptionHelper.ThrowHDiffPatchInputPathNotExist(inputFile.FullName); + if (inputFile.Length != patchMetadata.DiffOldSize) + throw ExceptionHelper.ThrowHDiffPatchInputSizeMismatched( + inputFile.FullName, inputFile.Length, patchMetadata.DiffOldSize); + + InputStream = new RandomMergedStreamWrapper([inputFile.FullName], [inputFile.Length]); + OutputStream = new RandomMergedStreamWrapper([outputFile.FullName], + [patchMetadata.DiffNewSize], + createFiles: true); + + return; + } + + // Return multiple stream for HDiff19 + DirectoryInfo inputDir = inputPath.GetDirectoryInfo(); + DirectoryInfo outputDir = outputPath.GetDirectoryInfo(); + outputDir.Create(); + + ref DirectoryPatchMetadata dirMetadata = ref Info.MetadataAs(); + if (Unsafe.IsNullRef(ref dirMetadata)) throw ExceptionHelper.ThrowHDiffInfoDirectoryPatchMetadataNotAllocated(); + + // Directory progress includes both the core/reference output and files + // copied unchanged from the input tree. + TotalSize = checked(dirMetadata.OutputPathCountSizeInfoP->Size + + dirMetadata.SameFilePathCountSizeInfoP->Size); + + // All input and output paths + Utf16UnmanagedString[] allInputPaths = CopyToManagedStringList(dirMetadata.InputPathListP); + Utf16UnmanagedString[] allOutputPaths = CopyToManagedStringList(dirMetadata.OutputPathListP); + + // Similar input and output paths + (string[] similarInputFiles, string[] similarOutputFiles) = + CopySimilarFilePairsFromIndexes(allInputPaths, allOutputPaths, ref dirMetadata); + + // Reference / Merged Input and Output paths + long refInputTotalSize = dirMetadata.InputPathCountSizeInfoP->Size; + int refInputCount = dirMetadata.InputFileIndexListP->Length; + string[] refInputFiles = new string[refInputCount]; + long[] refInputFilesSize = new long[refInputCount]; + + int refOutputCount = dirMetadata.OutputFileIndexListP->Length; + string[] refOutputFiles = new string[refOutputCount]; + long[] refOutputFilesSize = new long[refOutputCount]; + + // -- Reference Input + Span refInputFileIndexSpan = dirMetadata.InputFileIndexListP->GetSpan(); + long lastInputFileEnds = 0; + for (int i = 0; i < refInputCount; i++) + { + int fileIndex = refInputFileIndexSpan[i]; + refInputFiles[i] = Path.GetFullPath(Path.Combine(inputDir.FullName, allInputPaths[fileIndex])); + + string filePath = refInputFiles[i]; + FileInfo fileInfo = new(filePath); + + if (!fileInfo.Exists) + throw ExceptionHelper.ThrowHDiffPatchInputPathNotExist(filePath); + + // -- Additional size check reference for Kuro Games HDiff format. + if (Info.InitializeOptions.IsKuroGamesHDiff && + dirMetadata.InputFileSizeListP != null) + { + ref long expectedOldRefSize = ref dirMetadata.InputFileSizeListP->GetSpan()[i]; + if (fileInfo.Length != expectedOldRefSize) + throw ExceptionHelper.ThrowHDiffPatchKuroInputFileSizeMismatched(filePath, fileInfo.Length, expectedOldRefSize); + } + + lastInputFileEnds += fileInfo.Length; + refInputFilesSize[i] = lastInputFileEnds; + } + + // -- Reference Input Size sanity + if (lastInputFileEnds != refInputTotalSize) + { + throw ExceptionHelper.ThrowHDiffPatchInputFilesMismatched(lastInputFileEnds, refInputTotalSize); + } + + // -- Reference Output + Span refOutputFileIndexSpan = dirMetadata.OutputFileIndexListP->GetSpan(); + Span refOutputFileSizeSpan = dirMetadata.OutputFileSizeListP->GetSpan(); + + long outputSize = 0; + for (int i = 0; i < refOutputCount; i++) + { + int fileIndex = refOutputFileIndexSpan[i]; + long fileSize = refOutputFileSizeSpan[i]; + + refOutputFiles[i] = Path.GetFullPath(Path.Combine(outputDir.FullName, allOutputPaths[fileIndex])); + outputSize += fileSize; + refOutputFilesSize[i] = outputSize; + } + + if (outputSize != patchMetadata.DiffNewSize) + { + throw new InvalidDataException( + $"The reference output size ({outputSize}) does not match the patch output size ({patchMetadata.DiffNewSize})."); + } + + InputStream = new RandomMergedStreamWrapper(refInputFiles, refInputFilesSize); + OutputStream = new RandomMergedStreamWrapper(refOutputFiles, + refOutputFilesSize, + createFiles: true); + _copySimilarFilesContext = new CopySimilarFilesContext(inputDir.FullName, similarInputFiles, + outputDir.FullName, similarOutputFiles); + } + + private static unsafe Utf16UnmanagedString[] CopyToManagedStringList(UnmanagedArray* unmanagedArray) + { + var strings = new Utf16UnmanagedString[unmanagedArray->Length]; + Span span = unmanagedArray->GetSpan(); + + span.CopyTo(strings); + return strings; + } + + private static unsafe (string[], string[]) CopySimilarFilePairsFromIndexes( + Span inputPaths, Span outputPaths, ref DirectoryPatchMetadata dirMetadata) + { + int count = dirMetadata.SameFilePathCountSizeInfoP->Count; + + string[] resultInputs = new string[count]; + string[] resultOutputs = new string[count]; + + for (int i = 0; i < count; i++) + { + ref FileIndexPair pair = ref dirMetadata.SameFilePathIndexPairP[i]; + resultInputs[i] = inputPaths[pair.OldIndex]; + resultOutputs[i] = outputPaths[pair.NewIndex]; + } + + return (resultInputs, resultOutputs); + } + + protected override void DisposeCore() + { + base.DisposeCore(); + _coverReader.Dispose(); + _rleCtrlReader.Dispose(); + _rleCodeReader.Dispose(); + _newDataReader.Dispose(); + } + +#if NET6_0_OR_GREATER + protected override ValueTask DisposeCoreAsync() + => new(Task.WhenAll(base.DisposeCoreAsync().AsTask(), + _coverReader.DisposeAsync().AsTask(), + _rleCtrlReader.DisposeAsync().AsTask(), + _rleCodeReader.DisposeAsync().AsTask(), + _newDataReader.DisposeAsync().AsTask())); +#endif +} diff --git a/SharpHPatchZ/Patch/PatcherBase.cs b/SharpHPatchZ/Patch/PatcherBase.cs new file mode 100644 index 0000000..788e83a --- /dev/null +++ b/SharpHPatchZ/Patch/PatcherBase.cs @@ -0,0 +1,67 @@ +using SharpHPatchZ.Header; +using SharpHPatchZ.IO.Reader; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpHPatchZ.Patch; + +internal abstract class PatcherBase + : IDisposable +#if NET6_0_OR_GREATER + , IAsyncDisposable +#endif +{ + protected RandomMergedStreamWrapper? InputStream { get; set; } + protected RandomMergedStreamWrapper? OutputStream { get; set; } + + protected HDiffInfo Info { get; set; } + protected PatchOptions Options { get; set; } + protected ProgressCallback ProgressCallback { get; } + + protected long TotalWritten; + protected long TotalSize; + + protected PatcherBase(HDiffInfo info, PatchOptions options, ProgressCallback progressCallback) + { + Info = info; + Options = options; + ProgressCallback = !progressCallback.IsAllocated ? new ProgressCallback() : progressCallback; + TotalSize = info.GetPatchMetadata().DiffNewSize; + } + + public abstract void StartPatch(string inputPath, string outputPath, CancellationToken token); + public abstract Task StartPatchAsync(string inputPath, string outputPath, CancellationToken token); + + internal +#if NET6_0_OR_GREATER + unsafe +#endif + void AdvanceProgress(int written) + { + Interlocked.Add(ref TotalWritten, written); + ProgressCallback.ProcessedBytesCallback(TotalWritten, TotalSize, written); + } + + public void Dispose() => DisposeCore(); + +#if NET6_0_OR_GREATER + public ValueTask DisposeAsync() => DisposeCoreAsync(); +#endif + + protected virtual void DisposeCore() + { + InputStream?.Dispose(); + OutputStream?.Dispose(); + } + +#if NET6_0_OR_GREATER + protected virtual ValueTask DisposeCoreAsync() + { + InputStream?.Dispose(); + OutputStream?.Dispose(); + + return ValueTask.CompletedTask; + } +#endif +} diff --git a/SharpHPatchZ/Patch/PatcherFactory.HDiff13Derived.cs b/SharpHPatchZ/Patch/PatcherFactory.HDiff13Derived.cs new file mode 100644 index 0000000..f9785dd --- /dev/null +++ b/SharpHPatchZ/Patch/PatcherFactory.HDiff13Derived.cs @@ -0,0 +1,191 @@ +using SharpHPatchZ.Header; +using SharpHPatchZ.Header.Metadata; +using SharpHPatchZ.IO.Compression; +using SharpHPatchZ.IO.Reader; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpHPatchZ.Patch; + +internal static partial class PatcherFactory +{ + private static class HDiff13Derived + { + public static HDiff13DerivedPatcher Create( + ref HDiffInfo info, + CreateStream createPatchStream, + PatchOptions options, + ProgressCallback progressCallback) + { + GetPatchContextInfos(ref info, + out long coverDataOffset, + out long rleCtrlDataOffset, + out long rleCodeDataOffset, + out long newDataOffset); + PatchMetadata patchMetadata = info.GetPatchMetadata(); + DiffReadersContext context = CreateReaderContext(info.CompressionType, + options, + patchMetadata, + createPatchStream(coverDataOffset), + createPatchStream(rleCtrlDataOffset), + createPatchStream(rleCodeDataOffset), + createPatchStream(newDataOffset)); + + return new HDiff13DerivedPatcher(context.Cover, + context.RleCtrl, + context.RleCode, + context.NewData, + info, + options, + progressCallback); + } + + public static async Task CreateAsync( + HDiffInfo info, + CreateStreamAsync createPatchStreamAsync, + PatchOptions options, + ProgressCallback progressCallback, + CancellationToken token) + { + GetPatchContextInfos(ref info, + out long coverDataOffset, + out long rleCtrlDataOffset, + out long rleCodeDataOffset, + out long newDataOffset); + PatchMetadata patchMetadata = info.GetPatchMetadata(); + DiffReadersContext context = CreateReaderContext(info.CompressionType, + options, + patchMetadata, + await createPatchStreamAsync(coverDataOffset, token), + await createPatchStreamAsync(rleCtrlDataOffset, token), + await createPatchStreamAsync(rleCodeDataOffset, token), + await createPatchStreamAsync(newDataOffset, token)); + + return new HDiff13DerivedPatcher(context.Cover, + context.RleCtrl, + context.RleCode, + context.NewData, + info, + options, + progressCallback); + } + + private static unsafe DiffReadersContext CreateReaderContext(HDiffCompression compType, + PatchOptions options, + PatchMetadata patchMetadata, + ValueTuple coverCtx, + ValueTuple rleCtrlCtx, + ValueTuple rleCodeCtx, + ValueTuple newDataCtx) + { + int bufferSize = options.ReaderBufferSize; + + ChunkSizeInfo coverSize = *patchMetadata.CoverDataSizeP; + ChunkSizeInfo controlSize = *patchMetadata.RleControlDataSizeP; + ChunkSizeInfo codeSize = *patchMetadata.RleCodeDataSizeP; + ChunkSizeInfo newDataSize = *patchMetadata.NewDiffDataSizeP; + + Stream[] decompressedStreams = + [ + CreateDecompressionStream(compType, coverSize, coverCtx), + CreateDecompressionStream(compType, controlSize, rleCtrlCtx), + CreateDecompressionStream(compType, codeSize, rleCodeCtx), + CreateDecompressionStream(compType, newDataSize, newDataCtx) + ]; + + EnableParallelDecompression(compType, + options, + decompressedStreams, + [coverSize, controlSize, codeSize, newDataSize]); + + BittableStreamReader coverReader = new(decompressedStreams[0], bufferSize, coverCtx.Item2); + BittableStreamReader rleCtrlReader = new(decompressedStreams[1], bufferSize, rleCtrlCtx.Item2); + BittableStreamReader rleCodeReader = new(decompressedStreams[2], bufferSize, rleCodeCtx.Item2); + BittableStreamReader newDataReader = new(decompressedStreams[3], bufferSize, newDataCtx.Item2); + + return new DiffReadersContext(coverReader, rleCtrlReader, rleCodeReader, newDataReader); + } + + private static void EnableParallelDecompression(HDiffCompression compType, + PatchOptions options, + Stream[] streams, + ChunkSizeInfo[] sizes) + { + if (compType is HDiffCompression.Uncompressed) + { + return; + } + + int requestedWorkers = options.ParallelThreads == 0 + ? Environment.ProcessorCount + : options.ParallelThreads > int.MaxValue + ? int.MaxValue + : (int)options.ParallelThreads; + int prefetchCount = Math.Min(streams.Length, Math.Max(0, requestedWorkers - 1)); + if (prefetchCount == 0) + { + return; + } + + List<(int Index, long CompressedSize)> candidates = new(streams.Length); + for (int index = 0; index < sizes.Length; index++) + { + if (sizes[index].CompressedSize > 0 && sizes[index].Size > 0) + { + candidates.Add((index, sizes[index].CompressedSize)); + } + } + + candidates.Sort(static (left, right) => right.CompressedSize.CompareTo(left.CompressedSize)); + prefetchCount = Math.Min(prefetchCount, candidates.Count); + + int prefetchBufferSize = options.ReaderBufferSize > 0 + ? Math.Max(64 << 10, options.ReaderBufferSize) + : 1 << 20; + for (int candidateIndex = 0; candidateIndex < prefetchCount; candidateIndex++) + { + int streamIndex = candidates[candidateIndex].Index; + streams[streamIndex] = new PrefetchedReadStream(streams[streamIndex], + prefetchBufferSize); + } + } + + private static Stream CreateDecompressionStream( + HDiffCompression compType, + ChunkSizeInfo size, + ValueTuple streamContext) + => DecompressStreamFactory.Create( + compType, + streamContext.Item1, + streamContext.Item2, + size.CompressedSize, + size.Size); + + private static unsafe void GetPatchContextInfos(ref HDiffInfo info, + out long coverDataOffset, + out long rleCtrlDataOffset, + out long rleCodeDataOffset, + out long newDataOffset) + { + ref PatchMetadata patchMetadata = ref info.GetPatchMetadata(); + + ChunkSizeInfo* coverDataSizeP = patchMetadata.CoverDataSizeP; + ChunkSizeInfo* rleControlDataSizeP = patchMetadata.RleControlDataSizeP; + ChunkSizeInfo* rleCodeDataSizeP = patchMetadata.RleCodeDataSizeP; + + coverDataOffset = patchMetadata.DiffDataOffset; + rleCtrlDataOffset = coverDataOffset + (coverDataSizeP->CompressedSize > 0 ? coverDataSizeP->CompressedSize : coverDataSizeP->Size); + rleCodeDataOffset = rleCtrlDataOffset + (rleControlDataSizeP->CompressedSize > 0 ? rleControlDataSizeP->CompressedSize : rleControlDataSizeP->Size); + newDataOffset = rleCodeDataOffset + (rleCodeDataSizeP->CompressedSize > 0 ? rleCodeDataSizeP->CompressedSize : rleCodeDataSizeP->Size); + } + + private record struct DiffReadersContext( + BittableStreamReader Cover, + BittableStreamReader RleCtrl, + BittableStreamReader RleCode, + BittableStreamReader NewData); + } +} diff --git a/SharpHPatchZ/Patch/PatcherFactory.cs b/SharpHPatchZ/Patch/PatcherFactory.cs new file mode 100644 index 0000000..c86972e --- /dev/null +++ b/SharpHPatchZ/Patch/PatcherFactory.cs @@ -0,0 +1,36 @@ +using System.Threading; +using System.Threading.Tasks; +using SharpHPatchZ.Extension; +using SharpHPatchZ.Header; + +namespace SharpHPatchZ.Patch; + +internal static partial class PatcherFactory +{ + public static PatcherBase CreateFromInfo(ref HDiffInfo info, + CreateStream createPatchStream, + PatchOptions options, + ProgressCallback progressCallback) + { + if (info.MagicType is HDiffMagic.HDiff19 or HDiffMagic.HDiff13) + { + return HDiff13Derived.Create(ref info, createPatchStream, options, progressCallback); + } + + throw ExceptionHelper.ThrowHDiffPatchFactoryNotSupported(info.MagicType); + } + + public static async Task CreateFromInfoAsync(HDiffInfo info, + CreateStreamAsync createPatchStreamAsync, + PatchOptions options, + ProgressCallback progressCallback, + CancellationToken token) + { + if (info.MagicType is HDiffMagic.HDiff19 or HDiffMagic.HDiff13) + { + return await HDiff13Derived.CreateAsync(info, createPatchStreamAsync, options, progressCallback, token); + } + + throw ExceptionHelper.ThrowHDiffPatchFactoryNotSupported(info.MagicType); + } +} diff --git a/SharpHPatchZ/Patch/RleCoverInfo.cs b/SharpHPatchZ/Patch/RleCoverInfo.cs new file mode 100644 index 0000000..6563318 --- /dev/null +++ b/SharpHPatchZ/Patch/RleCoverInfo.cs @@ -0,0 +1,83 @@ +using SharpHPatchZ.Extension; +using SharpHPatchZ.Header; +using SharpHPatchZ.Header.Metadata; +using SharpHPatchZ.IO.Reader; +using System; +using System.Runtime.InteropServices; +using System.Threading; + +namespace SharpHPatchZ.Patch; + +[StructLayout(LayoutKind.Sequential)] +internal struct RleCoverInfo +{ + public long OldStreamPosition; + public long NewStreamPosition; + public long RleLength; + public long CopyLength; + + public RleCoverInfo() { } + + public RleCoverInfo(long oldStreamPosition, + long newStreamPosition, + long rleLength, + long copyLength) + { + OldStreamPosition = oldStreamPosition; + NewStreamPosition = newStreamPosition; + RleLength = rleLength; + CopyLength = copyLength; + } + + internal static NativeMemoryBuffer Read(BittableStreamReader reader, + HDiffInfo info, + CancellationToken token) + { + ref PatchMetadata patchMetadata = ref info.GetPatchMetadata(); + + int rleCoverCount = patchMetadata.CoverDataCount; + NativeMemoryBuffer backedBuffer = new(rleCoverCount); + + try + { + Span covers = backedBuffer.Span; + long lastOldPosBack = 0; + long lastNewPosBack = 0; + + for (int i = 0; i < rleCoverCount; i++) + { + token.ThrowIfCancellationRequested(); + + long oldPosBack = lastOldPosBack; + long newPosBack = lastNewPosBack; + + long incOldPos = reader.ReadLong7Bit(BittableStreamReader.KSignTagBit); + + byte incOldPosSign = (byte)(reader.PreviousByte >> (8 - BittableStreamReader.KSignTagBit)); + long oldPos = incOldPosSign == 0 ? oldPosBack + incOldPos : oldPosBack - incOldPos; + + long copyLength = reader.ReadLong7Bit(); + long coverLength = reader.ReadLong7Bit(); + + oldPosBack = oldPos; + newPosBack += copyLength; + oldPosBack += coverLength; + + covers[i] = new RleCoverInfo(oldPos, newPosBack, coverLength, copyLength); + newPosBack += coverLength; + + lastOldPosBack = oldPosBack; + lastNewPosBack = newPosBack; + } + + return backedBuffer; + } + catch + { + backedBuffer.Dispose(); + throw; + } + } + + public override string ToString() => $"RleLength: {RleLength} | CopyLength: {CopyLength} | OldPos: {OldStreamPosition} | NewPos: {NewStreamPosition}"; +} diff --git a/SharpHPatchZ/PatchOptions.cs b/SharpHPatchZ/PatchOptions.cs new file mode 100644 index 0000000..ea64b1b --- /dev/null +++ b/SharpHPatchZ/PatchOptions.cs @@ -0,0 +1,81 @@ +using System; +using System.Runtime.InteropServices; + +namespace SharpHPatchZ; + +/// +/// Specifies options during patching operations. This options contains some essential settings to determine the buffer size and parallelization. +/// +[StructLayout(LayoutKind.Sequential)] +public struct PatchOptions() +{ + /// + /// Default and optimal for both performance and memory allocation. + /// + public static readonly PatchOptions Default = new() + { + ParallelThreads = (uint)Environment.ProcessorCount, +#if NET6_0_OR_GREATER + UseSIMD = true +#endif + }; + + /// + /// Prioritizes performance by allocating much bigger sequential buffer for the reader, patch workers and the copy routine + /// + public static readonly PatchOptions BigBuffer = Default with + { + ReaderBufferSize = 1 << 20, + CopyBufferSize = 128 << 10, + PatchWorkerBufferSize = 16 << 20 + }; + + /// + /// Maintaining smaller memory allocation possible while impacting the performance. + /// + public static readonly PatchOptions SmallBuffer = Default with + { + ReaderBufferSize = 4 << 10, + CopyBufferSize = 4 << 10, + PatchWorkerBufferSize = 128 << 10 + }; + + /// + /// Optimized for Hard Drives where it requires sequential read/write routines and avoiding random seek by reducing the amount of patch workers. + /// + public static readonly PatchOptions OptimizeForHDD = Default with + { + ParallelThreads = 1, + CopyBufferSize = BigBuffer.CopyBufferSize + }; + +#if NET6_0_OR_GREATER + /// + /// Whether to use SIMD on the RLE addition functions.
+ /// If enabled, AVX2 will be used by default on supported CPUs.
+ /// Otherwise, falling back to SSE2 for supported x86-64-v2 CPUs.
+ /// If none of the above are available, fallback to Runtime-Vectorization or Scalar on ARM or unsupported CPUs + ///
+ public bool UseSIMD = true; +#endif + + /// + /// Determines how much the maximum parallel threads being used to produce the patch workers. + /// + public uint ParallelThreads = 0; + + /// + /// Determines how much buffer to be allocated for the RLE Stream Readers. + /// + public int ReaderBufferSize = 0; + + /// + /// Determines how much buffer to be allocated for the same-file copy operation. + /// + public int CopyBufferSize = 0; + + /// + /// Determines how much buffer to be allocated for each of the patch workers. + /// + public int PatchWorkerBufferSize = 0; +} diff --git a/SharpHPatchZ/PatchResult.cs b/SharpHPatchZ/PatchResult.cs new file mode 100644 index 0000000..506688d --- /dev/null +++ b/SharpHPatchZ/PatchResult.cs @@ -0,0 +1,27 @@ +using System; +using SharpHPatchZ.Extension; + +namespace SharpHPatchZ; + +/// +/// Determines whether the patch process has been successful or not. +/// +public class PatchResult +{ + /// + /// Containing an error if the patching process is faulty. + /// + public Exception? Exception { get; init; } + + /// + /// Whether the patching process is successful or faulty. + /// + public bool IsSuccessful { get; init; } + + public static implicit operator bool(PatchResult result) => result.IsSuccessful; + public static implicit operator Exception?(PatchResult result) => result.Exception; + public static implicit operator int(PatchResult result) => ExceptionHelper.TryGetReturnCodeFromError(result); + + public static implicit operator PatchResult(Exception? ex) => new() { Exception = ex, IsSuccessful = ex == null }; + public static implicit operator PatchResult(bool value) => new() { Exception = !value ? new Exception() : null, IsSuccessful = value }; +} diff --git a/SharpHPatchZ/ProgressCallback.cs b/SharpHPatchZ/ProgressCallback.cs new file mode 100644 index 0000000..e219009 --- /dev/null +++ b/SharpHPatchZ/ProgressCallback.cs @@ -0,0 +1,91 @@ +#if NET6_0_OR_GREATER +using System.Runtime.CompilerServices; +#endif +using System.Runtime.InteropServices; + +namespace SharpHPatchZ; + +/// +/// A callback delegate for the progress of the patching. +/// +/// Determines how many bytes already processed. +/// Determines how many bytes to be processed in total. +/// How many bytes is currently being written into the disk. +public delegate void ProcessedBytesManagedCallback(long totalProcessed, long totalSize, int written); + +/// +/// Sets progress callback for the patching process. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly +#if NET6_0_OR_GREATER + unsafe +#endif + struct ProgressCallback +{ + /// + /// The delegate callback for the progress method. + /// +#if NET6_0_OR_GREATER + internal readonly delegate* unmanaged[Cdecl] ProcessedBytesCallback; +#else + internal readonly ProcessedBytesManagedCallback ProcessedBytesCallback; +#endif + + /// + /// Whether the callback pointer is allocated or not. + /// + internal bool IsAllocated => ProcessedBytesCallback != null; + + /// + /// Creates a new default struct with a NOP callback. + /// + public ProgressCallback() : this(null) { } + + /// + /// Creates a new with specified callback method. + /// + /// +#if NET6_0_OR_GREATER + public ProgressCallback(ProcessedBytesManagedCallback? callback) + { + if (callback != null) + { + nint callbackPtr = Marshal.GetFunctionPointerForDelegate(callback); + ProcessedBytesCallback = (delegate* unmanaged[Cdecl])callbackPtr; + return; + } + + ProcessedBytesCallback = &NopProcessedBytesCallback; + } +#else + public ProgressCallback(ProcessedBytesManagedCallback? callback) + { + if (callback != null) + { + ProcessedBytesCallback = callback; + return; + } + + ProcessedBytesCallback = NopProcessedBytesCallback; + } +#endif + + /// + /// This method is expected to be exists as a NOP method as a placeholder. + /// The field on .NET 6 uses an unmanaged delegate pointer to the method since it cannot be null. + /// So the field is pointing to this method if no callback delegate is set. + /// +#if NET6_0_OR_GREATER + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + [SkipLocalsInit] +#endif + private static void NopProcessedBytesCallback(long totalProcessed, long totalSize, int written) { } + + /// + /// Creates a new with specified callback method. + /// + /// A method used for the callback of the progress + /// A new struct. + public static ProgressCallback CreateFromManaged(ProcessedBytesManagedCallback callback) => new(callback); +} diff --git a/SharpHDiffPatch.Core/SharpHDiffPatch.Core.csproj b/SharpHPatchZ/SharpHPatchZ.csproj similarity index 51% rename from SharpHDiffPatch.Core/SharpHDiffPatch.Core.csproj rename to SharpHPatchZ/SharpHPatchZ.csproj index 5a26910..87d85a6 100644 --- a/SharpHDiffPatch.Core/SharpHDiffPatch.Core.csproj +++ b/SharpHPatchZ/SharpHPatchZ.csproj @@ -1,48 +1,56 @@  + - SharpHDiffPatch.Core - SharpHDiffPatch - A port of HPatchZ for HDiff file patching implementation written in C# - SharpHDiffPatch is a patching library for HDiffPatch format written in C#, purposedly as a port of HPatchZ implementation. Supporting file and directory patching with BZip2, Deflate, Zstd, LZMA2 (not LZMA) and No compression diff format. + SharpHPatchZ + SharpHPatchZ (formerly: SharpHDiffPatch) - Apply binary patches for HDiffPatch format. + SharpHPatchZ is a port of HPatchZ implementation from HDiffPatch project (by housisong), written in C#. Provides as nearly as fast as the native implementation, supporting multi-threading and several compressed formats. https://github.com/CollapseLauncher/SharpHDiffPatch.Core MIT Copyright (c) 2025 Collapse Project Team, Kemal Setya Adhi (neon-nyan) neon-nyan - netstandard2.0;net6.0;net7.0;net8.0;net9.0;net10.0 + netstandard2.0;net6.0;net7.0;net8.0;net9.0;net10.0;net11.0 icon.png true README.md - 2.4.1 - 2.4.1 + 3.0.0 + 3.0.0 true true Debug;Release true hdiff - AnyCPU;x64;x86;ARM64 - 12 + enable + 14 + + + + + - + - - + + - - + + + + - - + True + diff --git a/SharpHPatchZ/SharpHPatchZ.h b/SharpHPatchZ/SharpHPatchZ.h new file mode 100644 index 0000000..6cf2bcf --- /dev/null +++ b/SharpHPatchZ/SharpHPatchZ.h @@ -0,0 +1,443 @@ +/* + * SharpHPatchZ native API + * + * This header describes the exports in HPatch.UnmanagedExtern.cs. The exports + * are available when SharpHPatchZ is published as a .NET 8+ NativeAOT shared + * library. + * + * SPDX-License-Identifier: MIT + */ + +#ifndef SHARP_HPATCH_Z_H +#define SHARP_HPATCH_Z_H + +#include +#include +#include + +/* + * Define SHPZ_STATIC when no import/export decoration is required. + * Define SHPZ_BUILD only while building the shared library itself. + */ +#ifndef SHPZ_API +# if defined(_WIN32) && !defined(SHPZ_STATIC) +# if defined(SHPZ_BUILD) +# define SHPZ_API __declspec(dllexport) +# else +# define SHPZ_API __declspec(dllimport) +# endif +# elif defined(__GNUC__) && defined(SHPZ_BUILD) +# define SHPZ_API __attribute__((visibility("default"))) +# else +# define SHPZ_API +# endif +#endif + +/* + * HPatch.UnmanagedExtern.cs uses cdecl unless its USEWINDOWS compilation + * symbol is defined. Define SHPZ_USE_STDCALL when consuming such a build. + */ +#ifndef SHPZ_CALL +# if defined(_WIN32) && defined(SHPZ_USE_STDCALL) +# define SHPZ_CALL __stdcall +# elif defined(_MSC_VER) +# define SHPZ_CALL __cdecl +# elif defined(__i386__) && (defined(__GNUC__) || defined(__clang__)) +# define SHPZ_CALL __attribute__((cdecl)) +# else +# define SHPZ_CALL +# endif +#endif + +/* Progress callbacks are always cdecl, including in stdcall API builds. */ +#ifndef SHPZ_CALLBACK +# if defined(_MSC_VER) +# define SHPZ_CALLBACK __cdecl +# elif defined(__i386__) && (defined(__GNUC__) || defined(__clang__)) +# define SHPZ_CALLBACK __attribute__((cdecl)) +# else +# define SHPZ_CALLBACK +# endif +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +/* .NET char is always a 16-bit UTF-16 code unit; wchar_t is not portable. */ +typedef uint16_t shpz_char16_t; + +/* Fixed-width representations of the C# enums used by the ABI. */ +typedef int32_t shpz_hdiff_magic; +enum { + SHPZ_HDIFF_MAGIC_UNKNOWN = 0, + SHPZ_HDIFF_MAGIC_HDIFF13 = 1, + SHPZ_HDIFF_MAGIC_HDIFF19 = 2 +}; + +typedef int32_t shpz_hdiff_compression; +enum { + SHPZ_HDIFF_COMPRESSION_UNCOMPRESSED = 0, + SHPZ_HDIFF_COMPRESSION_LZMA = 1, + SHPZ_HDIFF_COMPRESSION_LZMA2 = 2, + SHPZ_HDIFF_COMPRESSION_ZLIB = 3, + SHPZ_HDIFF_COMPRESSION_PBZ2 = 4, + SHPZ_HDIFF_COMPRESSION_BZ2 = 5, + SHPZ_HDIFF_COMPRESSION_ZSTD = 6 +}; + +typedef int32_t shpz_hdiff_checksum; +enum { + SHPZ_HDIFF_CHECKSUM_NONE = 0, + SHPZ_HDIFF_CHECKSUM_FADLER64 = 1, + SHPZ_HDIFF_CHECKSUM_CRC32 = 2 +}; + +typedef int16_t shpz_metadata_type; +#define SHPZ_METADATA_MARKER ((shpz_metadata_type)(uint16_t)0x8080u) +#define SHPZ_METADATA_PATCH ((shpz_metadata_type)(uint16_t)0xC080u) +#define SHPZ_METADATA_DIRECTORY_PATCH ((shpz_metadata_type)(uint16_t)0xA080u) +#define SHPZ_METADATA_CHECKSUM_DATA ((shpz_metadata_type)(uint16_t)0x9080u) +#define SHPZ_METADATA_ARRAY ((shpz_metadata_type)(uint16_t)0x8880u) +#define SHPZ_METADATA_UTF16_STRING ((shpz_metadata_type)(uint16_t)0x8480u) + +typedef int32_t shpz_last_error_message_type; +enum { + SHPZ_LAST_ERROR_MESSAGE = 1, + SHPZ_LAST_ERROR_STACK_TRACE = 2, + SHPZ_LAST_ERROR_MESSAGE_AND_TRACE = 3 +}; + +/* Values returned by the exported functions. */ +enum { + SHPZ_SUCCESS = 0, + SHPZ_ERROR_UNKNOWN = -1, + + SHPZ_ERROR_HEADER_MAGIC_NOT_SUPPORTED = 0x10, + SHPZ_ERROR_COMPRESSION_NOT_SUPPORTED = 0x11, + SHPZ_ERROR_CHECKSUM_NOT_SUPPORTED = 0x12, + SHPZ_ERROR_PATCH_FACTORY_NOT_SUPPORTED = 0x13, + + SHPZ_ERROR_INFO_NOT_ALLOCATED = 0x30, + SHPZ_ERROR_DIRECTORY_METADATA_NOT_ALLOCATED = 0x31, + SHPZ_ERROR_PATCH_METADATA_NOT_ALLOCATED = 0x32, + SHPZ_ERROR_FILE_DESCRIPTOR_NULL = 0x33, + SHPZ_ERROR_ARGUMENT_NULL = 0x34, + + SHPZ_ERROR_HEADER_SIGNATURE_UNREADABLE = 0x50, + SHPZ_ERROR_END_OF_FILE_OR_DATA = 0x51, + SHPZ_ERROR_PATH_INVALID = 0x52, + SHPZ_ERROR_IO = 0x53, + SHPZ_ERROR_INPUT_PATH_NOT_FOUND = 0x54, + SHPZ_ERROR_INPUT_SIZE_MISMATCH = 0x55, + SHPZ_ERROR_PATH_NOT_DIRECTORY = 0x56, + SHPZ_ERROR_PATH_NOT_FILE = 0x57, + SHPZ_ERROR_INPUT_FILES_MISMATCH = 0x58, + SHPZ_ERROR_KURO_INPUT_SIZE_MISMATCH = 0x59, + SHPZ_ERROR_STREAM_READ_OUT_OF_BOUNDS = 0x5A, + SHPZ_ERROR_STRING_ENCODING = 0x5B, + + SHPZ_ERROR_LZMA_PROPERTY_MISSING = 0xA0, + SHPZ_ERROR_LZMA2_DICTIONARY_INVALID = 0xA1, + SHPZ_ERROR_LZMA2_NO_COMPRESSED_PAYLOAD = 0xA2, + SHPZ_ERROR_LZMA_DICTIONARY_LENGTH_INVALID = 0xA3, + SHPZ_ERROR_LZMA_DATA_TOO_SMALL = 0xA4 +}; + +/* Match StructLayout(LayoutKind.Sequential), whose default packing is 8. */ +#if defined(_MSC_VER) || defined(__GNUC__) || defined(__clang__) +# pragma pack(push, 8) +#endif + +typedef struct shpz_initialize_options { + int32_t is_kuro_games_hdiff; +} shpz_initialize_options; + +typedef struct shpz_patch_options { + uint8_t use_simd; + uint8_t _padding0[3]; + uint32_t parallel_threads; + int32_t reader_buffer_size; + int32_t copy_buffer_size; + int32_t patch_worker_buffer_size; +} shpz_patch_options; + +typedef void (SHPZ_CALLBACK *shpz_processed_bytes_callback)( + int64_t total_processed, + int64_t total_size, + int32_t written); + +typedef struct shpz_progress_callback { + shpz_processed_bytes_callback processed_bytes; +} shpz_progress_callback; + +typedef struct shpz_hdiff_info { + shpz_hdiff_magic magic_type; + shpz_hdiff_compression compression_type; + shpz_hdiff_checksum checksum_type; + shpz_initialize_options initialize_options; + void *metadata; +} shpz_hdiff_info; + +typedef struct shpz_chunk_size_info { + int64_t size; + int64_t compressed_size; +} shpz_chunk_size_info; + +typedef struct shpz_entry_count_size_info { + int32_t count; + int64_t size; +} shpz_entry_count_size_info; + +typedef struct shpz_file_index_pair { + int32_t old_index; + int32_t new_index; +} shpz_file_index_pair; + +typedef struct shpz_extern_size_info { + int32_t new_execute_count; + int64_t private_reserved_data_size; + int64_t private_extern_data_size; + int64_t extern_data_size; +} shpz_extern_size_info; + +typedef struct shpz_native_string_utf16 { + shpz_char16_t *chars; + int32_t length; +} shpz_native_string_utf16; + +typedef struct shpz_utf16_string { + shpz_metadata_type metadata_type; + uint8_t is_initialized; + uint8_t is_disposed; + shpz_native_string_utf16 native; +} shpz_utf16_string; + +/* These three structs mirror different closed forms of UnmanagedArray. */ +typedef struct shpz_utf16_string_array { + shpz_metadata_type metadata_type; + uint8_t is_initialized; + uint8_t is_disposed; + int32_t length; + int32_t element_size; + shpz_utf16_string *data; +} shpz_utf16_string_array; + +typedef struct shpz_int32_array { + shpz_metadata_type metadata_type; + uint8_t is_initialized; + uint8_t is_disposed; + int32_t length; + int32_t element_size; + int32_t *data; +} shpz_int32_array; + +typedef struct shpz_int64_array { + shpz_metadata_type metadata_type; + uint8_t is_initialized; + uint8_t is_disposed; + int32_t length; + int32_t element_size; + int64_t *data; +} shpz_int64_array; + +typedef struct shpz_checksum_data_info { + shpz_metadata_type metadata_type; + uint8_t is_initialized; + uint8_t is_disposed; + int32_t element_size; + int32_t element_count; + uint8_t *data; +} shpz_checksum_data_info; + +typedef struct shpz_patch_metadata { + shpz_metadata_type metadata_type; + uint8_t is_initialized; + uint8_t is_disposed; + int64_t diff_new_size; + int64_t diff_old_size; + int64_t diff_data_offset; + int32_t cover_data_count; + shpz_chunk_size_info *cover_data_size; + shpz_chunk_size_info *rle_control_data_size; + shpz_chunk_size_info *rle_code_data_size; + shpz_chunk_size_info *new_diff_data_size; +} shpz_patch_metadata; + +typedef struct shpz_directory_patch_metadata { + shpz_metadata_type metadata_type; + uint8_t is_initialized; + uint8_t is_disposed; + uint8_t is_input_directory; + uint8_t is_output_directory; + + shpz_entry_count_size_info *input_path_count_size; + shpz_entry_count_size_info *output_path_count_size; + shpz_entry_count_size_info *same_file_path_count_size; + shpz_file_index_pair *same_file_path_index_pairs; + + shpz_utf16_string_array *input_paths; + shpz_utf16_string_array *output_paths; + shpz_int32_array *input_file_indices; + shpz_int64_array *input_file_sizes; + shpz_int32_array *output_file_indices; + shpz_int64_array *output_file_sizes; + shpz_int64_array *output_file_hashes; + + shpz_extern_size_info *extern_size; + shpz_chunk_size_info *head_data_size; + shpz_patch_metadata *patch_metadata; + shpz_checksum_data_info *checksum_data; + shpz_int32_array *new_execute_indices; +} shpz_directory_patch_metadata; + +#if defined(_MSC_VER) || defined(__GNUC__) || defined(__clang__) +# pragma pack(pop) +#endif + +/* Convenient initializers matching the unmanaged entry points' defaults. */ +static inline shpz_initialize_options shpz_make_initialize_options(void) +{ + shpz_initialize_options value = { 0 }; + return value; +} + +static inline shpz_patch_options shpz_make_patch_options(void) +{ + shpz_patch_options value = { 0 }; + value.use_simd = 1; + return value; +} + +static inline shpz_progress_callback shpz_make_progress_callback( + shpz_processed_bytes_callback callback) +{ + shpz_progress_callback value; + value.processed_bytes = callback; + return value; +} + +/* + * String arguments documented as "auto string" accept a null-terminated UTF-8 + * string or a null-terminated UTF-16LE string. UTF-16 must use shpz_char16_t, + * not wchar_t on platforms where wchar_t is 32 bits. + */ + +SHPZ_API int32_t SHPZ_CALL shpz_read_header_signature_string( + const void *signature, + shpz_hdiff_magic *magic_type, + shpz_hdiff_compression *compression_type, + shpz_hdiff_checksum *checksum_type); + +/* + * On success, info owns metadata allocated by SharpHPatchZ. Release it exactly + * once with shpz_free_diff_info. The memory buffer only needs to remain valid + * for the duration of this call. + */ +SHPZ_API int32_t SHPZ_CALL shpz_init_from_memory( + const uint8_t *data, + int64_t data_length, + shpz_hdiff_info *info, + const shpz_initialize_options *options); + +SHPZ_API int32_t SHPZ_CALL shpz_init_from_filepath( + const void *patch_path, + shpz_hdiff_info *info, + const shpz_initialize_options *options); + +/* The FILE remains owned by the caller and is not closed by SharpHPatchZ. */ +SHPZ_API int32_t SHPZ_CALL shpz_init_from_FILE( + FILE *patch_file, + shpz_hdiff_info *info, + const shpz_initialize_options *options); + +SHPZ_API int32_t SHPZ_CALL shpz_patch_from_filepath( + const void *patch_path, + const void *input_path, + const void *output_path, + const shpz_hdiff_info *info, + const shpz_patch_options *options, + const shpz_progress_callback *progress); + +/* The FILE remains owned by the caller and is not closed by SharpHPatchZ. */ +SHPZ_API int32_t SHPZ_CALL shpz_patch_from_FILE( + FILE *patch_file, + const void *input_path, + const void *output_path, + const shpz_hdiff_info *info, + const shpz_patch_options *options, + const shpz_progress_callback *progress); + +SHPZ_API int32_t SHPZ_CALL shpz_free_diff_info(shpz_hdiff_info *info); + +/* Returned pointers are borrowed and become invalid after free_diff_info. */ +SHPZ_API const shpz_patch_metadata *SHPZ_CALL +shpz_util_get_patch_metadata(const shpz_hdiff_info *info); + +SHPZ_API const shpz_directory_patch_metadata *SHPZ_CALL +shpz_util_get_directory_patch_metadata(const shpz_hdiff_info *info); + +/* + * buffer_length is measured in bytes for A and in UTF-16 code units for W. + * The return value excludes the terminating NUL. A return value of -1 means + * that the supplied buffer was too small or conversion failed. Last-error + * state is shared by the process rather than stored per thread. + */ +SHPZ_API int32_t SHPZ_CALL shpz_get_last_errorA( + uint8_t *buffer, + int32_t buffer_length, + shpz_last_error_message_type message_type); + +SHPZ_API int32_t SHPZ_CALL shpz_get_last_errorW( + shpz_char16_t *buffer, + int32_t buffer_length, + shpz_last_error_message_type message_type); + +#if defined(__cplusplus) +} /* extern "C" */ +#endif + +/* Fail compilation early if consumer options changed an ABI-sensitive layout. */ +#if defined(__cplusplus) && __cplusplus >= 201103L +# define SHPZ_STATIC_ASSERT(condition, message) static_assert(condition, message) +#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L +# define SHPZ_STATIC_ASSERT(condition, message) _Static_assert(condition, message) +#endif + +#if defined(SHPZ_STATIC_ASSERT) +SHPZ_STATIC_ASSERT(sizeof(shpz_hdiff_magic) == 4, "shpz_hdiff_magic must be 4 bytes"); +SHPZ_STATIC_ASSERT(sizeof(shpz_metadata_type) == 2, "shpz_metadata_type must be 2 bytes"); +SHPZ_STATIC_ASSERT(sizeof(shpz_initialize_options) == 4, "shpz_initialize_options layout mismatch"); +SHPZ_STATIC_ASSERT(sizeof(shpz_patch_options) == 20, "shpz_patch_options layout mismatch"); +SHPZ_STATIC_ASSERT(sizeof(shpz_progress_callback) == sizeof(void *), "shpz_progress_callback layout mismatch"); +SHPZ_STATIC_ASSERT(sizeof(shpz_hdiff_info) == (sizeof(void *) == 8 ? 24 : 20), + "shpz_hdiff_info size mismatch"); +SHPZ_STATIC_ASSERT(offsetof(shpz_hdiff_info, metadata) == 16, "shpz_hdiff_info layout mismatch"); +SHPZ_STATIC_ASSERT(sizeof(shpz_chunk_size_info) == 16, "shpz_chunk_size_info layout mismatch"); +SHPZ_STATIC_ASSERT(sizeof(shpz_entry_count_size_info) == 16, "shpz_entry_count_size_info layout mismatch"); +SHPZ_STATIC_ASSERT(offsetof(shpz_entry_count_size_info, size) == 8, "8-byte field alignment mismatch"); +SHPZ_STATIC_ASSERT(sizeof(shpz_file_index_pair) == 8, "shpz_file_index_pair layout mismatch"); +SHPZ_STATIC_ASSERT(sizeof(shpz_extern_size_info) == 32, "shpz_extern_size_info layout mismatch"); +SHPZ_STATIC_ASSERT(sizeof(shpz_native_string_utf16) == sizeof(void *) * 2, + "shpz_native_string_utf16 layout mismatch"); +SHPZ_STATIC_ASSERT(sizeof(shpz_utf16_string) == (sizeof(void *) == 8 ? 24 : 12), + "shpz_utf16_string layout mismatch"); +SHPZ_STATIC_ASSERT(sizeof(shpz_utf16_string_array) == (sizeof(void *) == 8 ? 24 : 16), + "shpz_utf16_string_array layout mismatch"); +SHPZ_STATIC_ASSERT(sizeof(shpz_int32_array) == (sizeof(void *) == 8 ? 24 : 16), + "shpz_int32_array layout mismatch"); +SHPZ_STATIC_ASSERT(sizeof(shpz_int64_array) == (sizeof(void *) == 8 ? 24 : 16), + "shpz_int64_array layout mismatch"); +SHPZ_STATIC_ASSERT(sizeof(shpz_checksum_data_info) == (sizeof(void *) == 8 ? 24 : 16), + "shpz_checksum_data_info layout mismatch"); +SHPZ_STATIC_ASSERT(sizeof(shpz_patch_metadata) == (sizeof(void *) == 8 ? 72 : 56), + "shpz_patch_metadata size mismatch"); +SHPZ_STATIC_ASSERT(offsetof(shpz_patch_metadata, diff_new_size) == 8, "shpz_patch_metadata layout mismatch"); +SHPZ_STATIC_ASSERT(sizeof(shpz_directory_patch_metadata) == (sizeof(void *) == 8 ? 136 : 72), + "shpz_directory_patch_metadata size mismatch"); +SHPZ_STATIC_ASSERT(offsetof(shpz_directory_patch_metadata, input_path_count_size) == 8, + "shpz_directory_patch_metadata layout mismatch"); +# undef SHPZ_STATIC_ASSERT +#endif + +#endif /* SHARP_HPATCH_Z_H */ diff --git a/SharpHDiffPatch.Core/icon.png b/SharpHPatchZ/icon.png similarity index 100% rename from SharpHDiffPatch.Core/icon.png rename to SharpHPatchZ/icon.png