前幾天,本人寫了幾個Delphi的Base64轉換函式,並寫了一篇部落格文章《Delphi版的Base64轉換函式 》,該文中,本人用Indy中的TIdHashMessageDigest5類,通過MD5的Hash驗證Base64編碼和解碼的正確性。事後,本人想驗證檔案的Base64編碼和解碼,並重新寫了Base64轉換函式,對整個檔案流的Base64編、解碼無疑也是正確的,見以下代碼(其中的Base64Encode和Base64Decode函數是重新寫的對流的Base64操作):
var
Source, Dest: TStream;
md5: TIdHashMessageDigest5;
Value: T4x4LongWordRecord;
s1, s2: string;
begin
if OpenDialog1.Execute then
begin
md5 := TIdHashMessageDigest5.Create;
try
Source := TFileStream.Create(OpenDialog1.FileName, fmOpenRead); // 開啟源檔案
Value := md5.HashValue(Source); // 取得源檔案Hash
s1 := md5.AsHex(Value);
Dest := TFileStream.Create('Base64EncodeTest.txt', fmCreate); // Base64編碼檔案
try
Base64Encode(Source, Dest);
finally
Source.Free;
Dest.Free;
end;
Source := TFileStream.Create('Base64EncodeTest.txt', fmOpenRead);// 開啟編碼檔案
Dest := TFileStream.Create('Base64DecodeTest.txt', fmCreate); // 還原源檔案內容
try
Base64Decode(Source, Dest);
Dest.Position := 0;
Value := md5.HashValue(Dest); // 取得還原檔案Hash
s2 := md5.AsHex(Value);
finally
Source.Free;
Dest.Free;
end;
finally
md5.Free;
end;
// 顯示源檔案和還原檔案的Hash十六進位字串,如果相等,證明編碼和解碼過程正確
ShowMessage(s1 + #10 + s2);
end;
end;
為了反覆驗證,又決定對流的部分內容進行驗證,在代碼Value := md5.HashValue(Source);前加了Source.Position := 100,代碼Base64Encode(Source, Dest)改為Base64Encode(Source, Dest, 100);(該函數有2個預設參數,可定義起始位置和長度)
表示從流的100位元組開始MD5Hash和Base64編碼,可是無論如何2次Hash不相等,而截取的檔案內容和長度無疑是正確的,通過反覆調試,最後發現問題在TIdHashMessageDigest5類上,請看Indy的TIdHashMessageDigest5類的源碼片斷:
function TIdHashMessageDigest4.HashValue(AStream: TStream): T4x4LongWordRecord;
Var
LStartPos: Integer;
LBitSize,
LSize: Int64;
S: String;
S1: String;
LFillSize : Integer;
begin
LStartPos := AStream.Position;
LSize := AStream.Size - LStartPos;
FBuffer := MD4_INIT_VALUES;
while LSize - AStream.Position >= SizeOf(FCBuffer) do
begin
AStream.Read(FCBuffer[0], SizeOf(FCBuffer));
MDCoder;
end;
// Ensure S1 has sufficient size to hold a complete 64-byte chunk
SetLength(S1, SizeOf(FCBuffer));
// Read the last set of bytes.
LStartPos := AStream.Read(S1[1], 64);
// Now adjust S1 to only hold the last set of bytes.
SetLength(S1, LStartPos);
(後面的代碼略)
問題出在while LSize - AStream.Position >= SizeOf(FCBuffer) do上,函數中,LSize := AStream.Size - LStartPos計算的是流的剩餘長度,如果LStartPos=0,則LSize為整個流的長度,這時運行是正確的,否則如果LSartPos<>0則是錯誤的!我測試時的流長度為1565,AStream.Position=100(即LStartPos=100),LSize=1465,正確的讀法應該是每次讀64位元組(Sizeof(FCBuffer)=64),讀22次後,餘57位元組。而按上面迴圈條件讀21次後,AStream.Position=21*64+100=1444,迴圈結束,餘21位元組,這就不正確了,而且當運行LStartPos := AStream.Read(S1[1], 64)時,由於流長度為1565,流的當前位置AStream.Position=1444,剩121位元組,所以讀出的位元組還是64,更是錯上加錯!
我又在Delphi2007中看了Indy9和Indy10的源碼,雖代碼有所改動,但迴圈原理一樣,由於MD5繼承Md4類,所以TIdHashMessageDigest4也是錯誤的,而且TIdHashMessageDigest2也是一樣!我將代碼稍稍改了一下:LSize := AStream.Size - LStartPos;改為LSize := AStream.Size; LBitSize := (AStream.Size - AStream.Position) * 8,後面的LBitSize計算句刪掉,重新運行,結果就正確了!
另外,即使上面改正正確了,但該類的流操作函數還是有缺陷的,只能定義流的開始位置,不能定義結束位置或者讀的長度。Indy10的Md5(md4)函數修改了參數,理應可以定義讀的長度,不料看了一下代碼,那個參數根本就沒有使用。
所以,在此提醒大家,一般使用Indy的Hash類是沒有問題的,特殊操作就不行了,這種BUG,開發人員們這些年都沒發現,真是吃驚!為了方便和正確起見,我只好著手自己寫這些類了。