Archived Forum Post

Index of archived forum posts

Question:

Chilkat.SFtp bug in ReadFileBytes when using Delphi XE3 with 32-bit Chilkat ActiveX imported TLB

Apr 18 '13 at 12:05

Hi!

I'm trying to implement streaming function of SFTP file download on Delphi XE3 using licensed 32-bit ActiveX SSH/SFTP component obtained from Chilkat (latest version 9.4.0).

Test condition: file size is 4096 bytes.

See the code below. When file size is exactly 4096 bytes (same as chunk size) the exception is thrown from ReadFileBytes method of SFtp.

uses CHILKATSSHLib_TLB;

function SFTP_FileDownload(const Host: string; Port: Integer; const Login, Password,
           RemoteURIPath, FileName, LocalPath: string; TimeoutSec: Integer): Boolean;
var
  SFTP: TChilkatSFtp;
  FileHandle: string;
  BEOF: Integer;
  TargetStream: TFileStream;
  NumRead: Integer;
  Buffer: array[0..4095] of Byte;
  DynBuffer: array of Byte;
begin
  Result := False;
  SFTP := TChilkatSFtp.Create(nil);
  try
    if SFTP.UnlockComponent('Test') <> 1 then Exit;
    SFTP.ConnectTimeoutMs := TimeoutSec * 1000;
    SFTP.IdleTimeoutMs := TimeoutSec * 1000;
    if SFTP.Connect(Host, Port) <> 1 then Exit;
    if SFTP.AuthenticatePw(Login, Password) <> 1 then Exit;
    if SFTP.InitializeSftp <> 1 then Exit;
    FileHandle := SFTP.OpenFile(RemoteURIPath + FileName, 'readOnly', 'openExisting');
    if Length(FileHandle) = 0 then Exit;
    try
      if not ForceDirectories(LocalPath) then Exit;
      TargetStream := TFileStream.Create(LocalPath + FileName, fmCreate);
      try
        BEOF := 0;
        while BEOF = 0 do
          begin
            //ReadFileBytes buggy call
            DynBuffer := SFTP.ReadFileBytes(FileHandle, 4096);
            //When file size is 4096 bytes the exception
            //is raised here (on the 2-nd iteration):
            //Exception class EVariantBadIndexError with
            //message 'Variant or safe array index out of bounds'.
            //
            if (SFTP.LastReadFailed(FileHandle) = 1) then Exit;
            NumRead := Length(DynBuffer);
            Move(Pointer(DynBuffer)^, Buffer, NumRead);
            TargetStream.Write(Buffer, NumRead);
            //
            //Also buggy
            BEOF := SFTP.Eof(FileHandle);
            //I also think Eof method has a bug. Because already on the
            //1-st iteration it is assumed to return 1. But it returns 0.
          end;
        Result := True;
      finally
        TargetStream.Free;
      end;
    finally
      SFTP.CloseHandle(FileHandle);
    end;
  finally
    SFTP.Disconnect;
    SFTP.Free;
  end;
end;

It would be quite nice to see comments from Chilkat.


Answer

In this case, the SFTP.ReadFileBytes would be returning a Variant containing a 0-length array. Specifically, it's a VARIANT containing a SAFEARRAY containing unsigned bytes (VT_UI1) where the length of the SAFEARRAY is 0.

I suspect the exception is from Delphi. It must not be able to handle a Variant containing a 0-length SAFEARRAY. A solution might be to declare DynBuffer as a Variant. Check it first for 0-length, and then if it contains bytes, assign it to a variable declared as "array of Byte".

See http://stackoverflow.com/questions/3619753/how-to-use-variant-arrays-in-delphi


Answer

OK, I'll try it.

But what about Eof method?

In case of file size of 4096 bytes and after successful reading of 4096 bytes in the first iteration the call of Eof is assumed to return 1, but it returns 0. I think that's not OKay.

Because of such Eof's behaviour I have this issue with ReadFileBytes in the 2-nd iteration. I thought that the 2-nd iteration should never occur normally.


Answer

Finally, below is the function that works well. Maybe it would be useful for one writing streaming SFTP downloads in Delphi.

function SFTP_FileDownload(const Host: string; Port: Integer; const Login, Password, RemoteURIPath, FileName, LocalPath: string; TimeoutSec: Integer): Boolean;
var
  SFTP: TChilkatSFtp;
  FileHandle: string;
  TargetStream: TFileStream;
  NumRead: Integer;
  Buffer: array[0..4095] of Byte;
  DynBuffer: array of Byte;
  ReadResult: OleVariant;
begin
  Result := False;
  SFTP := TChilkatSFtp.Create(nil);
  try
    if SFTP.UnlockComponent('Test') <> 1 then Exit;
    SFTP.ConnectTimeoutMs := TimeoutSec * 1000;
    SFTP.IdleTimeoutMs := TimeoutSec * 1000;
    if SFTP.Connect(Host, Port) <> 1 then Exit;
    if SFTP.AuthenticatePw(Login, Password) <> 1 then Exit;
    if SFTP.InitializeSftp <> 1 then Exit;
    FileHandle := SFTP.OpenFile(RemoteURIPath + FileName, 'readOnly', 'openExisting');
    if Length(FileHandle) = 0 then Exit;
    try
      if not ForceDirectories(LocalPath) then Exit;
      TargetStream := TFileStream.Create(LocalPath + FileName, fmCreate);
      try
        repeat
          ReadResult := SFTP.ReadFileBytes(FileHandle, SizeOf(Buffer));
          if VarArrayHighBound(ReadResult, 1) < 0 then Break;
          if (SFTP.LastReadFailed(FileHandle) = 1) then Exit;
          DynBuffer := ReadResult;
          NumRead := Length(DynBuffer);
          Move(Pointer(DynBuffer)^, Buffer, NumRead);
          TargetStream.Write(Buffer, NumRead);
        until SFTP.Eof(FileHandle) = 1;
        Result := True;
      finally
        TargetStream.Free;
      end;
    finally
      SFTP.CloseHandle(FileHandle);
    end;
  finally
    SFTP.Disconnect;
    SFTP.Free;
  end;
end;