diff --git a/Demos/HashBenchmark_FMX/MainFormHashBenchmark.pas b/Demos/HashBenchmark_FMX/MainFormHashBenchmark.pas
index be9b529..2b9b942 100644
--- a/Demos/HashBenchmark_FMX/MainFormHashBenchmark.pas
+++ b/Demos/HashBenchmark_FMX/MainFormHashBenchmark.pas
@@ -217,7 +217,6 @@ procedure TFormMain.RunBenchmark(ClassName: string; RowIndex: Integer);
Iterations : UInt32;
BufferSize : UInt32;
Salt : TBytes;
- PwdHashBuf : TBytes;
begin
Hash := TDECHash.ClassByName(ClassName).Create;
@@ -233,8 +232,6 @@ procedure TFormMain.RunBenchmark(ClassName: string; RowIndex: Integer);
(TDECPasswordHash(Hash).MaxPasswordLength < BufferSize) then
begin
BufferSize := TDECPasswordHash(Hash).MaxPasswordLength;
- SetLength(PwdHashBuf, BufferSize);
- Move(FBenchmarkBuffer[0], PwdHashBuf[0], BufferSize);
// Since password hashes take quite long time to calculate limit that time
// by limiting the number of iterations calculated but in such a way that
@@ -267,12 +264,11 @@ procedure TFormMain.RunBenchmark(ClassName: string; RowIndex: Integer);
if Hash.IsPasswordHash then
TDECPasswordHash(Hash).Salt := Salt;
- if not Hash.IsPasswordHash then
- HashResult := Hash.CalcBytes(FBenchmarkBuffer)
- else
- HashResult := Hash.CalcBytes(PwdHashBuf);
-// Former implementation, but this leads to crashes:
-// HashResult := Hash.CalcBuffer(@FBenchmarkBuffer[0], BufferSize);
+ // CalcBuffer(const Buffer; Size) already takes Buffer by reference.
+ // Do NOT write CalcBuffer(@FBenchmarkBuffer[0], …) — the extra @ makes
+ // the engine read from a stack temp (pointer-to-pointer), which caused
+ // intermittent OutOfRange/AVs in Absorb for large buffers (GitHub #94).
+ HashResult := Hash.CalcBuffer(FBenchmarkBuffer[0], BufferSize);
end;
FStopwatch.Stop;
diff --git a/Source/DECHash.pas b/Source/DECHash.pas
index f63a943..c74e529 100644
--- a/Source/DECHash.pas
+++ b/Source/DECHash.pas
@@ -341,9 +341,13 @@ THash_SHA3Base = class(TDECHashBit)
end;
///
- /// Buffer type
+ /// View type for sponge Absorb over an arbitrary-length message.
+ /// Must not be limited to 64 KiB: with {$R+} enabled for the SHA3 block,
+ /// indexing past 65535 raised ERangeError (GitHub issue #94 —
+ /// HashBenchmark / 1 MiB Keccak). The bound is only for typing; Absorb
+ /// never allocates a TBABytes instance.
///
- TBABytes = array[0..65535] of UInt8;
+ TBABytes = array[0..High(Integer) div SizeOf(UInt8) - 1] of UInt8;
///
/// Pointer to a buffer
///
@@ -5131,24 +5135,28 @@ procedure THash_SHA3Base.AbsorbQueue;
procedure THash_SHA3Base.Calc(const Data; DataSize: Integer);
var
- DataPtr : PBABytes;
+ DataPtr : PByte;
RoundSize : UInt32;
const
- // Maximum number of bytes one can process in one round
- MaxRoundSize = MaxInt div 8;
+ // Max bytes per Absorb round. Must keep RoundSize*8 within Int32 (Absorb bit
+ // length). Kept well below MaxInt/8 so multi-round walks are realistic for
+ // large messages (HashBenchmark 1 MiB). DataPtr must be PByte so Inc advances
+ // by bytes — PBABytes would scale by SizeOf(TBABytes) (GitHub #94 / CR).
+ cMaxBytesPerRound = 64 * 1024;
begin
// due to the way the inherited calc is constructed it must not be called here!
if (DataSize > 0) then
begin
- DataPtr := PBABytes(@Data);
+ // Byte-addressed pointer: Inc(DataPtr, n) must advance n bytes.
+ DataPtr := @Data;
while (UInt32(DataSize) > 0) do
begin
RoundSize := DataSize;
- if (RoundSize > MaxRoundSize) then
- RoundSize := MaxRoundSize;
+ if (RoundSize > cMaxBytesPerRound) then
+ RoundSize := cMaxBytesPerRound;
- Absorb(DataPtr, RoundSize * 8);
+ Absorb(PBABytes(DataPtr), RoundSize * 8);
Dec(DataSize, RoundSize);
Inc(DataPtr, RoundSize);
end;
diff --git a/Unit Tests/Tests/TestDECHashSHA3.pas b/Unit Tests/Tests/TestDECHashSHA3.pas
index 04729b2..e2a0a50 100644
--- a/Unit Tests/Tests/TestDECHashSHA3.pas
+++ b/Unit Tests/Tests/TestDECHashSHA3.pas
@@ -350,6 +350,11 @@ TestTHash_Keccak_224 = class(TestTHash_Keccak_Base)
procedure TestIdentity;
procedure TestFinalByteLength;
procedure TestFinalByteLengthOverflow;
+ ///
+ /// Regression for GitHub #94: Absorb must accept messages larger than
+ /// 64 KiB under range checks (HashBenchmark uses 1 MiB).
+ ///
+ procedure TestCalcBufferLargeMessage;
end;
// Test methods for class THash_Keccak_256
@@ -2036,6 +2041,63 @@ procedure TestTHash_Keccak_224.TestIsPasswordHash;
CheckNotEquals(true, FHash.IsPasswordHash);
end;
+procedure TestTHash_Keccak_224.TestCalcBufferLargeMessage;
+const
+ // Larger than cMaxBytesPerRound (64 KiB) in THash_SHA3Base.Calc so the
+ // multi-round DataPtr walk is exercised (regression for PBABytes Inc scaling).
+ cSize = 1024 * 1024; // same as HashBenchmark_FMX cBufferSize
+ cIncrementalChunk = 4096; // each Init/Calc piece stays single-round
+ // PyCryptodome Crypto.Hash.keccak, digest_bits=224, over 0..255 repeating
+ cExpectedHex: RawByteString =
+ '7746d1b9b33662006e83d88443fc956ee1e01b8d058b8227a9d1e66b';
+var
+ Buf, DigestBuf, DigestBytes, DigestIncremental: TBytes;
+ i, n, Offset, Take: Integer;
+ Hex: RawByteString;
+begin
+ SetLength(Buf, cSize);
+ n := 0;
+ for i := 0 to cSize - 1 do
+ begin
+ Buf[i] := n;
+ Inc(n);
+ if n > 255 then
+ n := 0;
+ end;
+
+ // One-shot paths (internal multi-round Absorb walk for 1 MiB)
+ DigestBuf := FHash.CalcBuffer(Buf[0], Length(Buf));
+ DigestBytes := FHash.CalcBytes(Buf);
+
+ // Incremental reference: many small Calc calls, each single-round only
+ FHash.Init;
+ Offset := 0;
+ while Offset < cSize do
+ begin
+ Take := cIncrementalChunk;
+ if Offset + Take > cSize then
+ Take := cSize - Offset;
+ FHash.Calc(Buf[Offset], Take);
+ Inc(Offset, Take);
+ end;
+ FHash.Done;
+ DigestIncremental := FHash.DigestAsBytes;
+
+ CheckEquals(Length(DigestBuf), Length(DigestBytes),
+ 'CalcBuffer and CalcBytes digest lengths must match');
+ CheckTrue(CompareMem(@DigestBuf[0], @DigestBytes[0], Length(DigestBuf)),
+ 'CalcBuffer and CalcBytes digests must match for 1 MiB input');
+ CheckEquals(Length(DigestBuf), Length(DigestIncremental),
+ 'One-shot and incremental digest lengths must match');
+ CheckTrue(CompareMem(@DigestBuf[0], @DigestIncremental[0], Length(DigestBuf)),
+ 'Multi-round one-shot Calc must match incremental small-chunk Calc '+
+ '(catches wrong DataPtr byte advance)');
+
+ Hex := BytesToRawString(TFormat_HEXL.Encode(DigestBuf));
+ CheckEquals(string(cExpectedHex), string(Hex),
+ 'Keccak-224 of 1 MiB (0..255 pattern) must match known digest');
+end;
+
{ TestTHash_Keccak_256 }
procedure TestTHash_Keccak_256.ConfigHashClass(HashClass: TDECHash;