Skip to content

Version 3 Rewriting - #11

Open
neon-nyan wants to merge 56 commits into
mainfrom
v3
Open

Version 3 Rewriting#11
neon-nyan wants to merge 56 commits into
mainfrom
v3

Conversation

@neon-nyan

@neon-nyan neon-nyan commented Aug 23, 2026

Copy link
Copy Markdown
Member

TODO: Description

Benchmark Result

Test: Single file, LZMA2 compressed

Method Mean Error StdDev Ratio RatioSD Gen0 Gen1 Gen2 Allocated Alloc Ratio
v2 19.46 ms 18.01 ms 0.987 ms 1.00 0.06 - - - 60.43 MB 1.00
v2+OldFastBuffer 57.55 ms 49.20 ms 2.697 ms 2.96 0.17 - - - 34.4 MB 0.57
v3 16.92 ms 18.52 ms 1.015 ms 0.87 0.06 468.7500 468.7500 468.7500 4.58 MB 0.08

Test: Directory, LZMA2 compressed

Method Mean Error StdDev Ratio Gen0 Gen1 Gen2 Allocated Alloc Ratio
v2 7.028 s 0.3049 s 0.0167 s 1.00 - - - 937.31 MB 1.00
v2+OldFastBuffer 7.176 s 0.2930 s 0.0161 s 1.02 - - - 692.19 MB 0.74
v3 4.639 s 0.3283 s 0.0180 s 0.66 3000.0000 2000.0000 2000.0000 136.46 MB 0.15

Test: Single file, LZMA2 compressed - Native (Result: 25 ms)

old : "G:\UnityPlayer-SEA.dll"
diff: "G:\UnityPlayer.lzma2.diff"
out : "G:\UnityPlayer-CN-native.dll"
  input oldDataSize: 30144448
       diffDataSize: 39523
       diffDataType: HDiff
  saved oldDataSize: 30144448
  saved newDataSize: 30144408
       compressType: "lzma2" (need decompress 4)

  patch progress: [==============================]  100.0%
  patch ok!

hpatchz time: 0.025 s

Test: Directory, LZMA compressed - Native (Result: 4.906 s)

old  dir: "G:\Hi3SEA\"
diffFile: "G:\Hi3SEAtoCNExecOnly.lzma2.diff"
out  dir: "G:\Hi3CN-native\"
         diffDataSize: 107925175
         diffDataType: DirHDiff
         checksumType: "fadler64"
         oldPathIsDir: true
         newPathIsDir: true
       new path count: 196 (fileCount:178)
  copy from old count: 122 (copySize: 86831653)
   ref old file count: 56 (oldRefSize: 799499793)
   ref new file count: 56 (newRefSize: 927363046)
           oldSumSize: 886331446
           newSumSize: 1014194699
       diffDataType: HDiff (in DirHDiff)
  saved oldDataSize: 799499793
  saved newDataSize: 927363046
       compressType: "lzma2" (need decompress 4)

hpatchz run with checksum plugin: "fadler64" (checksumSets: new copy)

  patch progress: [==============================]  100.0%
  patch ok!

hpatchz dir patch time: 4.906 s

SharpHDiffPatch -> SharpHPatchZ
Attempt to implement a thread-safe Merged Stream Wrapper for Read/Write operations.

Warning: Most of the Code for the reader are vibe-coded. So, need to check it further.
+ Struct type adjustment inside DirectoryPatchMetadata
  + Use UnmanagedArray<T> for some input-output ref lists instead of a direct array pointer
+ Fix CreateUnmanagedInt64List and CreateUnmanagedInt64As32List reading on backed numbers
+ Add extern functions:
    - shpz_get_last_errorA
    - shpz_get_last_errorW
    - shpz_patch_from_filepath
    - shpz_patch_from_FILE
+ Make some string arguments in extern functions auto-detect the encoding of the string
    - shpz_read_header_signature_string
    - shpz_init_from_filepath
    - shpz_patch_from_filepath
+ Make the ExceptionHelper throw functions returns the Exception instead of throwing within
+ Add implicit cast for Utf16UnmanagedString and NativeStringW
+ Remove unnecessary type size check on UnmanagedArray<T>.GetSpan()
+ Use numbered error code on ExceptionHelper instead of flags
+ Add callback allocation check on ProgressCallback to avoit ExecutionEngineException due to attempt on calling null callback.
Use .NET Framework 4.7.2 for testing the lowest version of .NET to be supported
Comment on lines +71 to +75
#if NET6_0_OR_GREATER
copyOverTcs.SetCanceled(token);
#else
copyOverTcs.SetCanceled();
#endif

This comment was marked as outdated.

Comment on lines +302 to +312
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
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The async implementation for reading index pairs is incompatible with the sync version. It reads data in the wrong order and incorrectly handles the signed delta encoding, causing corrupted patch metadata.
Severity: CRITICAL

Suggested Fix

The async implementation in CreateUnmanagedIndexPairListAsync must be updated to match the logic of the synchronous version. This includes reading newIndex before oldIndex and correctly implementing the signed delta decoding for oldIndex by checking the tag bit, as is done in the synchronous path.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: SharpHPatchZ/Extension/StreamExtension.cs#L302-L312

Potential issue: The asynchronous method for reading file index pairs is inconsistent
with its synchronous counterpart. The async version reads `oldIndex` and `newIndex`
values in a different order and, more importantly, fails to account for the signed delta
encoding used for `oldIndex`. The synchronous implementation correctly handles this
encoding by checking a tag bit for the sign. This discrepancy will lead to corrupted
metadata when creating directory patches via the async path, causing the patcher to
reference incorrect files, which can result in silent data corruption or out-of-bounds
access errors.

Also affects:

  • SharpHPatchZ/Extension/StreamExtension.cs:337~353

return -1;
}

splitFirst = splitFirst.Slice(1, message.Length - 2);

This comment was marked as outdated.

public static InvalidOperationException ThrowHDiffPatchPathNotAFile(string path)
=> new($"[{Const.HDiffPatchPathNotAFile}] Path is not a file!: {path}");
public static InvalidOperationException ThrowHDiffPatchInputFilesMismatched(long existingSize, long expectingSize)
=> new($"[{Const.HDiffPatchInputFilesMismatched}] Input file size mismatched! Expecting: {expectingSize} bytes but got: {expectingSize} bytes instead.");

This comment was marked as outdated.

while (totalRead < minimumBytes)
{
int read = stream.Read(buffer, totalRead, count - totalRead);
if (read == 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: In ReadAtLeast, totalRead is incorrectly initialized to offset instead of 0, causing incorrect behavior when reading from a stream with a non-zero offset on netstandard2.0.
Severity: MEDIUM

Suggested Fix

Initialize totalRead to 0. The call to stream.Read should use offset + totalRead for the buffer position to ensure data is written to the correct location.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: SharpHPatchZ/Extension/StreamExtension.cs#L65

Potential issue: In the pre-NET6.0 implementation of `ReadAtLeast`, the `totalRead`
variable, which tracks the number of bytes read, is incorrectly initialized to the
`offset` parameter instead of 0. When `ReadExactly` is called with a non-zero `offset`,
this causes the loop condition `while (totalRead < minimumBytes)` to evaluate
incorrectly, potentially causing the read loop to terminate prematurely or not execute
at all. While current internal usage passes an offset of 0, this is a latent bug in the
public API for the `netstandard2.0` target framework that would affect external
consumers.

Comment on lines +653 to +656
if (encodedLength == long.MaxValue)
{
throw new InvalidDataException("An RLE run length exceeds the supported range.");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The RLE decoder in EnsureRun checks for long.MaxValue to detect an overflow, but the ReadLong7BitCore method it calls returns 0 on overflow, causing the check to fail.
Severity: MEDIUM

Suggested Fix

Update the overflow check in RleDecoder.EnsureRun to compare the encodedLength against 0 instead of long.MaxValue to correctly handle the sentinel value returned on overflow.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: SharpHPatchZ/Patch/HDiff13DerivedPatcher.CorePatcher.cs#L653-L656

Potential issue: The `ReadLong7BitCore` method returns `0` as a sentinel value to
indicate an integer overflow during decoding. However, its caller,
`RleDecoder.EnsureRun`, incorrectly checks for `long.MaxValue` to detect this overflow.
This mismatch means that if a malformed patch file triggers an overflow, the error is
not caught. The code then proceeds with an `encodedLength` of `0`, causing the RLE
decoder to process an incorrect number of bytes. This can lead to data corruption in the
output before a later validation step eventually fails.

+ Reenable PrefetchedReadStream for Zstd
Comment on lines +152 to +162
{
kvp.Value.Value.Dispose();
}
}
_fileStreams.Clear();
}
finally
{
_lifetimeLock.ExitWriteLock();
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The Dispose() method in RandomMergedStreamWrapper does not dispose its _lifetimeLock field, causing a ReaderWriterLockSlim kernel resource leak on every patch operation.
Severity: HIGH

Suggested Fix

In the Dispose() method of RandomMergedStreamWrapper, call _lifetimeLock.Dispose() to ensure the underlying kernel resources held by the ReaderWriterLockSlim are properly released. This should be done within a finally block after the write lock is released.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: SharpHPatchZ/IO/Reader/RandomMergedStreamWrapper.cs#L139-L162

Potential issue: The `Dispose()` method in the `RandomMergedStreamWrapper` class fails
to call `Dispose()` on its `_lifetimeLock` field, which is a `ReaderWriterLockSlim`
instance. Since `ReaderWriterLockSlim` holds underlying kernel resources, this omission
results in a resource leak. Each time a `RandomMergedStreamWrapper` is disposed, a
kernel handle is leaked. In long-running applications or batch processes that perform
many patching operations, this accumulation of leaked handles can exhaust system limits,
leading to an `OutOfResourceException` and application failure.

Comment on lines 163 to 167
{
_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)
{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: A new null check in LzmaInputStream can mask errors from malformed streams, causing silent data corruption instead of throwing an exception.
Severity: MEDIUM

Suggested Fix

Remove the _decoder != null check from the _decoder.Code(...) call. The existing logic should already guarantee that _decoder is not null for valid streams, and throwing a NullReferenceException for malformed streams is the correct behavior to prevent silent data corruption.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: SharpHPatchZ/IO/Compression/Lzma/LzmaInputStream.cs#L163-L167

Potential issue: In `LzmaInputStream`, a null check was added before calling
`_decoder.Code(...)`. For well-formed LZMA2 streams, the `_decoder` is guaranteed to be
initialized. However, if a malformed or corrupted stream is processed, this new check
will cause the code to silently skip the decoding step instead of throwing an exception
as it would have previously. This behavior masks the data corruption error and can lead
to the generation of silently corrupted output, which is a more severe issue than an
explicit failure.

Comment on lines +102 to +104
=> Task.Factory.StartNew(_ => StartPatch(inputPath, outputPath, token),
token,
TaskCreationOptions.LongRunning);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Task.Factory.StartNew is called with the CancellationToken passed as the state object, not as a cancellation token, and without specifying a TaskScheduler.
Severity: HIGH

Suggested Fix

Update the Task.Factory.StartNew call to use the correct overload that accepts a CancellationToken and explicitly specifies TaskScheduler.Default. This ensures the task runs on a background thread and correctly handles pre-cancellation. For example: Task.Factory.StartNew(() => StartPatch(inputPath, outputPath, token), token, TaskCreationOptions.LongRunning, TaskScheduler.Default);.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: SharpHPatchZ/Patch/HDiff13DerivedPatcher.cs#L102-L104

Potential issue: The `StartPatchAsync` method uses an incorrect overload of
`Task.Factory.StartNew`. The `CancellationToken` is passed as the `state` object instead
of being registered with the task factory. This bypasses pre-cancellation checks,
causing the task to be scheduled even if the token is already cancelled. Additionally,
no `TaskScheduler` is specified, so the task defaults to `TaskScheduler.Current`. If
this method is called from a UI thread, the long-running patch operation will execute on
that thread, freezing the user interface.

@socket-security

socket-security Bot commented Aug 24, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedmicrosoft.netframework.referenceassemblies@​1.0.310010090100100

View full report

@socket-security

socket-security Bot commented Aug 24, 2026

Copy link
Copy Markdown

All alerts resolved. Learn more about Socket for GitHub.

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

View full report

Comment thread SharpHPatchZ/IO/Reader/DecompressionTransitionStream.cs
+ HPatch.TryGetHDiff13PatchMetadata (Extern: shpz_util_get_hdiff13_patch_metadata)
+ HPatch.TryGetHDiff19DirectoryPatchMetadata (Extern: shpz_util_get_hdiff19_patch_metadata)
Comment thread SharpHPatchZ/HPatch.UnmanagedExtern.cs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant