有没有简单的方法解决 Delphi utf8 文件缺陷?

13
我已经(艰辛地)发现了这样一个问题,如果一个文件有有效的UTF-8 BOM但包含任何无效的UTF8编码,并且被读取到任何具备编码能力的Delphi(2009+)方法中,如LoadFromFile,那么结果就是一个完全空的文件而没有错误指示。在我的几个应用程序中,我宁愿简单地失去一些糟糕的编码,即使在这种情况下我也不会得到错误报告。
调试显示MultiByteToWideChar被调用两次,首先是为了获取输出缓冲区大小,然后进行转换。 但是TEncoding.UTF8包含一个私有的FMBToWCharFlags值用于这些调用,并且它使用MB_ERR_INVALID_CHARS值进行初始化。因此,获取字符计数的调用返回0,加载的文件完全为空。 不使用标志调用此API会“默默地删除非法代码点”。
我的问题是如何最好地穿过编码区域的嵌套,以解决这是一个私有值的事实(并且需要这样做,因为它是所有线程的类变量)。 我认为可以添加一个自定义的UTF8编码,使用Marco Cantu的Delphi 2009书中的指导。并且它可以选择在调用没有标志的MultiByteToWideChar再次调用之后引发编码错误的异常。 但这并不能解决如何让我的自定义编码代替Tencoding.UTF8使用的问题。
如果我可以在初始化时将其设置为应用程序的默认值,或者通过实际修改Tencoding.UFT8的类变量来设置,那么这可能已足够。
当然,我需要一种解决方案,而不必等待提出一个更健壮的设计的QC报告,获得接受,并看到它改变。
任何想法都非常欢迎。有人可以确认这是否仍然是XE4的问题吗?我尚未安装。

1
如果您有答案,请将其发布为答案,而不是问题的编辑。否则,该问题将永远保持开放状态,没有答案。 - Celada
4个回答

12

在我首次更新Indy以支持TEncoding时,我遇到了MB_ERR_INVALID_CHARS问题,并最终实现了一个自定义的派生自TEncoding类的类来处理UTF-8,以避免指定MB_ERR_INVALID_CHARS。 我没有考虑使用类助手。

但是,这个问题不仅仅局限于UTF-8。 任何TEncoding类的解码失败都将导致结果为空,而不是引发异常。 Embarcadero选择这种方式,而大多数RTL / VCL使用异常,这超出了我的理解。 不在错误上引发异常会导致Indy中要解决的很多问题。


2
+1 派生自己的自定义 TEncoding 显然是你应该做的。 - David Heffernan
1
TEncoding中存在不少的设计和实现问题,因此在Indy 10.6中,我决定完全放弃TEncoding并编写自己的基于接口的框架来替换它。 - Remy Lebeau
@David:如果LoadFromFile检测到BOM,那么你如何获得所使用的编码呢?你是否需要读取前三个字节,然后针对任何发现的UTF8文件传递一个编码参数? - frogb
@frogb:是的,你需要这样做。TEncoding不允许用户定义的类被注册到其默认BOM处理逻辑中。 - Remy Lebeau
@remy:谢谢。我本来会接受你的答案,对于维护Indy的人来说,它显然是正确的;但我的答案更适合我,并且更接近我的原始问题。常常发生的情况是,提出问题可以帮助你自己找到答案! - frogb
显示剩余2条评论

3
这可以很简单地完成,至少在Delphi XE5中是如此(我没有检查过早期版本)。只需实例化您自己的TUTF8Encoding
procedure LoadInvalidUTF8File(const Filename: string);
var
  FEncoding: TUTF8Encoding;
begin
  FEncoding := TUTF8Encoding.Create(CP_UTF8, 0, 0); 
                      // Instead of CP_UTF8, MB_ERR_INVALID_CHARS, 0
  try
    with TStringList.Create do
    try
      LoadFromFile(Filename, FEncoding);
      // ...
    finally
      Free;
    end;
  finally
    FEncoding.Free;
  end;
end;

这里唯一的问题是,新创建的 TUTF8EncodingIsSingleByte 属性被错误地设置为 False,但是目前在 Delphi 源代码中没有任何地方使用该属性。

不幸的是,如果您知道文件包含无效字符,那么该解决方案只有用处。我们的软件只需要处理Unicode、UTF8和系统默认编码,因此真正的问题在于加载没有编码参数的文件。VCL在所有情况下都“工作”,除非一个正确检测为具有UTF8 BOM的文件包含无效的UTF8序列。这样的文件最终被加载为空。 - frogb
1
True -- 这个解决方案假定您已知道编码为UTF-8,因此如果您尝试通过BOM或内容来嗅探编码,则不适用。 - Marc Durdin

1
一个部分的解决方法是强制使用UTF8编码来全局抑制MB_ERR_INVALID_CHARS。对我来说,这避免了需要引发异常的需要,因为我发现它使MultiByteToWideChar并不完全“沉默”:它实际上插入了$fffd字符(Unicode“代替字符”),我可以在这些情况下找到它们。以下代码执行此操作:
unit fixutf8;
interface
uses System.Sysutils;
type
  TUTF8fixer = class helper for Tmbcsencoding
  public
    procedure setflag0;
  end;

implementation
procedure TUTF8fixer.setflag0;
{$if CompilerVersion = 31}
asm
  XOR ECX,ECX
  MOV Self.FMBToWCharFlags,ECX
end;
{$else}
begin
  Self.FMBToWCharFlags := 0;
end;
{$endif}

procedure initencoding;
begin
  (Tencoding.UTF8 as TmbcsEncoding).setflag0;
end;

initialization
  initencoding;
end.

一个更有用和原则性的修复需要改变对MultiByteToWideChar的调用,不再使用MB_ERR_INVALID_CHARS,并且进行初始调用时使用此标志,以便在加载完成后引发异常,表明字符将被替换。
这个问题有相关的QC报告,包括76571、79042和111980。第一个已经被解决为“按设计”。
(编辑以适用于Delphi Berlin)

直到 Delphi 10.1,您可以使用 class helper for Tmbcsencoding public property UnicodeFlags: cardinal read FMBToWCharFlags write FMBToWCharFlags end;,然后使用 initialization Tencoding.UTF8.UnicodeFlags := 0; end. - Arioch 'The
如果通过其他方式获取TUTF8Encoding对象而不是使用TEncoding.GetUTF8,例如在XE2中使用TEncoding.GetEncoding(CP_UTF8)将创建TUTF8Encoding的新实例而不是本地实例,则也无法正常工作。 - Arioch 'The
条件编译的目的是为了保留早于柏林版本的代码原始发布解决方案,使用最初实现的代码助手。我没有确定未来编译器应该做什么,因为即使是 ASM 解决方案在未来的版本中也可能被关闭。 - frogb
正如我下面所解释的那样,接受的代码的目的是修复内置的UTF8检测。我对获取新的编码对象没有兴趣。但还是谢谢。 - frogb
您无法确保使用的库不会进行检测操作来获取信息。这些“新对象”也是以完全相同的方式进行内置检测的。此外,如果任何库出于任何原因调用标准的FreeEncodings方法并重新创建对象。 - Arioch 'The

0

你的“全局”方法并不真正全局——它依赖于这样一个假设,即所有代码都只使用同一个TUTF8Encoding实例。同一个实例,你在其中篡改了标志字段。

但是,如果通过其他方式获取TUTF8Encoding对象(例如在XE2中使用另一种方法TEncoding.GetEncoding(CP_UTF8)),而不是TEncoding.GetUTF8,那么它将无法工作——它会创建一个新的TUTF8Encoding实例,而不是重用FUTF8共享实例。或者某些函数可能直接运行TUTF8Encode.Create

因此,我建议采用另外两种方法。

一种是通过修补类实现的方法,有点像黑客。你可以引入自己的类来获得新的“修复”构造函数体。

type TMyUTF8Encoding = class(TUTF8Encoding)
  public constructor Create; override;
end;

这个构造函数将是TUTF8Encoding.Create()实现的复制品,除了您想要设置的标志(在XE2中通过调用另一个继承的Create(x,y,z)来完成,因此您不需要访问私有字段)。

然后,您可以修补存储的TUTF8Encoding VMT,覆盖其虚拟构造函数为您的新构造函数。

您可以阅读Delphi文档,了解“内部格式”等内容,以获取VMT布局。在修补之前,您还需要调用VirtualProtect(或其他特定于平台的函数)来从VMT内存区域中删除保护,然后再恢复它。

学习示例

或者您可以尝试使用Delphi Detours库,希望它可以修补虚拟构造函数。然后...在这里使用那个相当复杂的库来达到单一目标可能有些过度。

在你修改了 TUTF8Encoding 类之后,调用 TEncoding.FreeEncodings 来移除已经创建的共享实例(如果有的话),从而触发使用你的修改重新创建 UTF8 实例。


然后,如果您将程序编译为单个整体EXE,而不使用运行时BPL模块,则只需将SysUtils.pas源文件复制到应用程序文件夹中,然后明确地将该本地副本包含到您的项目中。

如何在Classes.pas中修补方法

在那里,您可以根据需要更改TUTF8Encoding实现方式,并使Delphi使用它。

然而,这种脑残式的简单(因此同样可靠)方法无法在您的项目构建为重用rtlNNN.bpl运行时包而不是整体时起作用。


感谢您的建议,我希望它们对其他人有用,但不幸的是它们没有提供我需要的任何东西。正如我在最初提出这个问题时所说,我从来不需要像您创建的MyEncoding这样的编码。我的问题核心是自动检测传递给我应用程序的文件的编码,而这些文件并不在我的控制之下。因此,我永远不需要提供编码。我只需要避免在读取到无效UTF8文件时出现异常或空文件。我接受的解决方案多年来一直很好用,这也是我标记它的原因。 - frogb
你没有完整地修补自动检测,而只是其中的一条路径。你正在以两个预言为基础来构建你的安全性:没有任何库会使用任何其他获取标准TUTF8Encoding对象的方法,也没有任何库会销毁你打补丁的单个TUTF8Encoding对象。这两个都是不可靠的依据,在99%的情况下可能有效,但在1%的情况下可能导致错误。而因为你有了“修补内置的UTF8检测”的虚假感觉(其实只是部分修补),所以你永远不会在确定忽略这些源头时遇到困难。 - Arioch 'The
作为您创建的MyEncoding,它只是一个跳板设备,使Delphi构建一个函数,然后在永久基础上将其注入到标准TUTF8Encoding中。您从未单独使用该类。您误解了重点 - 应该修补TUTF8Encoding类,而不是它的实例。MyEncoding类不是要像@Marc Durdin答案中那样使用的类,您永远不会实例化它,它只是提供固定代码以修补内置类。 - Arioch 'The
感谢您再次提供的评论。 - frogb

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接