Delphi 7 中的 Indy 10.6.2 idFTP - 接收文件时文件名中的本机符号问题 - DefStringEncoding 的行为可能不好

问题描述

我将 Indy 10.6.2 与 Delphi 7 和 Windows 10 64 位波兰语一起使用。

我运行一个 FTP 服务器:FTPServer: TIdFTPServer,并且通过使用 FTPClient: TIdFTP,我试图获取一个在其文件名中包含国家符号的文件。不幸的是,在调用 Get() 函数后,在服务器端的 OnRetrieveFile 事件中,而不是 AFileName 中的国家符号,我以问号结束,这显然会导致其他异常。

为了测试,我在同一台机器和同一个应用程序中同时运行服务器和客户端,以消除任何其他干扰。

在客户端,我已经尝试过:

 FTPClient.DefStringEncoding := IndyTextEncoding_UTF8;
 FTPClient.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
 FTPClient.IOHandler.DefAnsiEncoding   := IndyTextEncoding_UTF8;

在服务器端,我尝试过:

 ASender.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
 ASender.Connection.IOHandler.DefAnsiEncoding   := IndyTextEncoding_UTF8;

它们都没有任何区别。我也尝试过其他编码,但都失败了。

下面是我的测试应用程序,以及客户端和服务器的文本 DFM 值。

unit Glowny_FTP;

interface

uses
  Windows,Messages,SysUtils,Variants,Classes,Graphics,Controls,Forms,Dialogs,StdCtrls,IdBaseComponent,IdComponent,IdTCPConnection,IdTCPClient,IdExplicitTLSClientServerBase,IdFTP,ZLibCompression,IdGlobal,IdioHandler,IdioHandlerStream,IdioHandlerSocket,IdioHandlerStack,IdCustomTcpserver,IdTcpserver,IdCmdTcpserver,IdFTPServer,IdContext,IdAntiFreezeBase,IdAntiFreeze;

type
  TForm1 = class(TForm)
    Button1: TButton;
    FTPClient: TIdFTP;
    FTPServer: TIdFTPServer;
    IdAntiFreeze1: TIdAntiFreeze;
    procedure Button1Click(Sender: TObject);
    procedure FTPServerUserLogin(ASender: TIdFTPServerContext;
      const AUsername,APassword: String; var AAuthenticated: Boolean);
    procedure FTPServerRetrieveFile(ASender: TIdFTPServerContext;
      const AFileName: WideString; var VStream: TStream);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);

begin

  FTPServer.DefaultPort    := 1350;
  FTPServer.Active         := True;

  FTPClient.Host     := '127.0.0.1';
  FTPClient.Port     := 1350;
  FTPClient.Username := 'new';
  FTPClient.Password := 'pass';
  FTPClient.Connect;
  // FTPClient.DefStringEncoding := IndyTextEncoding_UTF8;
  // FTPClient.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
  // FTPClient.IOHandler.DefAnsiEncoding   := IndyTextEncoding_UTF8;

  FTPClient.Get('sample ąęśćłó','C:\węzły.cpy',True,False);   // <-- filename containing national symbols
  FTPClient.disconnect;
end;

procedure TForm1.FTPServerUserLogin(ASender: TIdFTPServerContext;
  const AUsername,APassword: String; var AAuthenticated: Boolean);
begin
  if (APassword='pass') then
    begin
      AAuthenticated := True;
    end else AAuthenticated := False;
end;

procedure TForm1.FTPServerRetrieveFile(ASender: TIdFTPServerContext;
  const AFileName: WideString; var VStream: TStream);
begin
 // ASender.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
 // ASender.Connection.IOHandler.DefAnsiEncoding   := IndyTextEncoding_UTF8;
  VStream := TFileStream.Create(AFileName+'.serv',fmOpenRead); //<-- here I get: "/sample ??????" without UTF8 and "/sample ????" with UTF8
end;

end.

也许还有有趣的 DFM 部分:

object FTPClient: TIdFTP
    Passive = True
    ConnectTimeout = 0
    DataPort = 20
    Password = 'TaJnEH1aS2loD3oSt4ePu'
    TransferType = ftBinary
    NATKeepAlive.UseKeepAlive = False
    NATKeepAlive.IdleTimeMS = 0
    NATKeepAlive.IntervalMS = 0
    ProxySettings.ProxyType = fpcmNone
    ProxySettings.Port = 0
    Left = 104
    Top = 72
  end
  object FTPServer: TIdFTPServer
    Bindings = <>
    DefaultPort = 1350
    TerminateWaitTime = 100
    CommandHandlers = <>
    ExceptionReply.Code = '500'
    ExceptionReply.Text.Strings = (
      'UnkNown Internal Error')
    Greeting.Code = '220'
    Greeting.Text.Strings = (
      'Indy FTP Server ready.')
    MaxConnectionReply.Code = '300'
    MaxConnectionReply.Text.Strings = (
      'Too many connections. Try again later.')
    ReplyTexts = <>
    ReplyUnkNownCommand.Code = '500'
    ReplyUnkNownCommand.Text.Strings = (
      'UnkNown Command')
    PathProcessing = ftppUnix
    AnonymousAccounts.Strings = (
      'anonymous'
      'ftp'
      'guest')
    OnUserLogin = FTPServerUserLogin
    OnRetrieveFile = FTPServerRetrieveFile
    SITECommands = <>
    MLSDFacts = []
    ReplyUnkNownSITCommand.Code = '500'
    ReplyUnkNownSITCommand.Text.Strings = (
      'Invalid SITE command.')
    Left = 104
    Top = 8
  end

更新:

经过雷米的帮助还是没有效果。在 FTPClient.Get() 中的 UTF8Encode() 之后,我在服务器端得到了更多的问号。现在我的代码如下所示: (我在 Form1.Caption 上检查 AFileName 只是为了快速调试)

...
FTPClient.Connect;

FTPClient.DefStringEncoding := IndyTextEncoding_UTF8;
FTPClient.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
FTPClient.IOHandler.DefAnsiEncoding   := IndyTextEncoding_8Bit;

FTPClient.Get(UTF8Encode('sample ąęśćłó'),False);
FTPClient.disconnect;

procedure TForm1.FTPServerRetrieveFile(ASender: TIdFTPServerContext;
  const AFileName: WideString; var VStream: TStream);
begin
  Form1.Caption := AFileName;
  VStream := TFileStream.Create('C:\tymczas z węzłami obl.aqr',fmOpenRead);
  //  VStream := TFileStream.Create(AFileName+'.serv',fmOpenRead);

end;

procedure TForm1.FTPServerConnect(AContext: TIdContext);
begin
  AContext.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
  AContext.Connection.IOHandler.DefAnsiEncoding   := IndyTextEncoding_UTF8;
end;

解决方法

背景

IOHandler 的 DefStringEncoding 需要设置为通过套接字传输字符串时要使用的字节编码。 Indy 将通过使用 DefStringEncoding 将其编码为字节来发送一个 Unicode 字符串。相反,Indy 将通过使用 DefStringEncoding 将接收到的字节解码为 Unicode 来读取 Unicode 字符串。

在 Delphi 2007 及更早版本中,涉及一个额外的步骤。 IOHandler 的 DefAnsiEncoding 需要设置为您要与 AnsiString 一起使用的编码。 Indy 将发送一个 AnsiString,首先使用 DefAnsiEncoding 将其从 ANSI 解码为 Unicode,然后使用 DefStringEncoding 发送该 Unicode。相反,Indy 将首先使用 AnsiString 读取 Unicode 字符串,然后使用 DefStringEncoding 将该 Unicode 编码为 ANSI 来读取 DefAnsiEncoding

默认情况下,DefStringEncodingIndyTextEncoding_ASCII(由于历史原因),而 DefAnsiEncodingIndyTextEncoding_OSDefault

在客户端

TIdFTP 如果确定服务器支持 UTF-8,将自动将 IOHandler 的 DefStringEncoding 切换为 IndyTextEncoding_UTF8。如果没有 UTF-8 扩展,FTP 协议需要使用 ASCII 来代替。因此,您根本不应该弄乱客户的 DefStringEncoding

您最初将远程文件名作为波兰语编码的 AnsiString 传递,因此将 DefAnsiEncoding 设置为 IndyTextEncoding_OSDefault 在设置为波兰语区域设置的操作系统上是有意义的。将 DefStringEncoding 设置为 IndyTextEncoding_UTF8,应将正确的 UTF-8 传输到服务器。

您后来将客户端更改为传入 UTF-8 编码的 AnsiString,但您将 DefAnsiEncoding 设置为 IndyTextEncoding_8Bit,因此 AnsiString 不会转换为Unicode 正确,因此后续转换为 UTF-8 格式不正确。在这种情况下,DefAnsiEncoding 需要设置为 IndyTextEncoding_UTF8。我建议将 DefAnsiEncoding 设置为 IndyTextEncoding_OSDefault 并使用操作系统区域设置编码的 AnsiString,除非您有令人信服的理由不这样做。

在服务器端

TIdFTPServer 只会在客户端发出 DefStringEncodingIndyTextEncoding_UTF8 命令(OPTS UTF-8 <NLST> ,但其他客户可能不会)。请注意,这不符合 RFC 2640,Indy 尚未完全实现。

您将 OPTS UTF8 ONTIdFTP 都设置为 DefStringEncoding,这在大多数情况下通常是可以的。在暴露对 DefAnsiEncoding 的访问权限的事件中,您最终会得到 UTF-8 编码的 IndyTextEncoding_UTF8

但是,AnsiString 事件在您的 Delphi 版本中使用 AnsiString 作为其 OnRetrieveFile 参数。服务器从套接字中读取文件名作为 WideString 后,它会按原样传递给事件处理程序,使用 RTL 自己的转换将其转换为 AFileName逻辑,默认情况下使用操作系统的语言环境。因此,在您的情况下,AnsiString 是 UTF-8 编码但操作系统区域设置是波兰语,您最终会得到格式错误的转换。幸运的是,您可以通过预先调用 RTL 的 SetMultiByteConversionCodePage() 函数来使用代码页 65001 (UTF-8) 执行 WideStringAnsiString 转换来缓解该问题。

然而,在 Delphi 2007 及更早版本中,AnsiString 的一些事件使用 WideString 参数,因此 TIdFTPServer 的内部有相当多的区域依赖于 RTL 的默认 WideStringTIdFTPServer 转换。因此,我建议在服务器端也将 AnsiString 设置为 WideString,除非您有令人信服的理由不这样做。如果您想强制 DefAnsiEncodingIndyTextEncoding_OSDefault 而不管 DefStringEncoding 命令,那没问题(前提是您的客户端只发送 UTF-8 编码的路径)。


话虽如此,试试这个:

IndyTextEncoding_UTF8

或者这个:

OPTS
,

最后我放弃了 D7 下乱七八糟的 TextEncoding。迷失了三天,我还在原地。

无论我选择哪种编码类型,长时间的调查都让我找到了 TIdASCIIEncoding.GetBytes()。这里任何高于 $007F (127) 的 Char 都变成了“?”这对于 ASCII 很明显,但对于 UTF8 和 ANSI 则不然。

只要我只在两个应用程序之间进行通信,我就会使用更长的路径来解决应该可以解决的问题。我知道它正在修补,但至少有效。

“解决方案”:

FTPClient.Get() 中,我将字符串编码为 IdEncoderMIME.EncodeString() 而不是包含国家符号的源字符串,在服务器端 FTPServerRetrieveFile() 中我删除第一个字符“/”然后我的字符串被解码IdDecoderMIME.DecodeString(),最后我在另一边得到了正确的文件名。

客户端:

S := IdEncoderMIME1.EncodeString('sample ąęśćłó',IndyTextEncoding_UTF8,IndyTextEncoding_UTF8);
FTPClient.Get(S,'C:\węzły.cpy',True,False);

服务器端:

S := Copy(AFileName,2,Length(AFileName));
S := IdDecoderMIME1.DecodeString(S,IndyTextEncoding_UTF8);

丑陋但有效...