2013-05-13 68 views
11

我发现(困难的方式),如果一个文件有一个有效的UTF-8 BOM但包含任何无效的UTF8编码,并且被任何Delphi(2009+)编码启用的方法(如LoadFromFile)读取,那么结果是一个完全空的文件,没有错误指示。在我的几个应用程序中,我宁愿仅仅丢失一些不好的编码,即使在这种情况下我也没有收到错误报告。有没有简单的方法来解决Delphi utf8文件的缺陷?

调试显示MultiByteToWideChar被调用两次,第一次获取输出缓冲区大小,然后进行转换。但是TEncoding.UTF8包含这些调用的私有值FMBToWCharFlags,并且这个值用MB_ERR_INVALID_CHARS的值初始化。所以获取charcount的调用返回0,并且加载的文件完全是空的。在没有标志的情况下调用这个API会'悄然丢弃非法代码点'。

我的问题是如何最好地编织通过在编码领域的类巢以解决这是一个私人价值(因为它是所有线程类var)的事实。我想我可以使用Marco Cantu的Delphi 2009书中的指导来添加自定义的UTF8编码。如果MultiByteToWideChar在没有该标志的情况下再次调用之后返回编码错误,它可以选择性地引发异常。但是这并不能解决如何使用自定义编码而不是Tencoding.UTF8的问题。

如果我可以在初始化时将它设置为应用程序的默认值,或许通过实际修改Tencoding.UFT8的类var,这可能就足够了。

当然,我需要一个解决方案,无需等待提交质量控制报告,询问更强大的设计,接受并更改设计。

任何想法都会非常受欢迎。有人可以证实,这仍然是XE4的问题,我还没有安装?

+1

如果您有答案,请将其作为回答发布,而不是作为问题的编辑。否则,这个问题将永远保持开放,没有答案。 – Celada 2013-05-14 01:01:11

回答

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,并且使这个标志的初始呼叫,这样的异常可以在加载后提高是完整的,表明字符将被替换。

在这个问题上有相关的质量控制报告,包括76571,79042和111980.第一个已经“按设计”解决。

(编辑用Delphi柏林工作)

+0

直到Delphi 10.1你可以'只为Tmbcsencoding类助手 公共属性UnicodeFlags:cardinal read FMBToWCharFlags写入FMBToWCharFlags结束;'然后使用'初始化Tencoding.UTF8.UnicodeFlags:= 0; ' – 2017-01-16 15:17:12

+0

如果通过除TEncoding.GetUTF8之外的其他方式获得'TUTF8Encoding'对象,例如在XE2中'TEncoding.GetEncoding(CP_UTF8)'将创建'TUTF8Encoding'的新实例,它也不会工作。 – 2017-01-16 15:47:49

+0

条件编译的目的是保留早于柏林的代码的原始发布解决方案,使用最初实现的代码助手。我不确定未来的编译器会做什么,因为即使ASM解决方案可能在未来版本中被关闭。 – frogb 2017-01-17 23:08:34

11

我跑进MB_ERR_INVALID_CHARS问题,当我第一次更新,印地支持TEncoding,并最终实现为UTF-8处理,以避免指定MB_ERR_INVALID_CHARS定制TEncoding派生类。我没想过要使用班级帮手。

但是,这个问题不仅限于UTF-8。任何TEncoding类的任何解码失败都会导致空白结果,而不是引发异常。为什么Embarcadero选择了这种路线,当大多数RTL/VCL使用异常时,这种情况超出了我的想象。没有提出错误例外导致Indy中相当多的问题必须解决。

+2

+1推导自己的自定义TEncoding显然是你应该做的。 – 2013-05-14 04:14:10

+1

'TEncoding'有很多设计和实现问题,所以在Indy 10.6中,我决定彻底删除'TEncoding',并编写我自己的基于接口的框架来替换它。 – 2013-05-14 08:11:29

+0

@David:当LoadFromFile检测到BOM时,您将如何获得您使用的编码?您是否必须读取前三个字节,然后为您找到的任何UTF8文件传递编码参数? – frogb 2013-05-14 09:01:53

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; 

这里唯一的问题是,IsSingleByte属性为新实例化TUTF8Encoding然后被错误地设置为False,但在Delphi源此属性当前未使用的任何地方。

+0

不幸的是,解决方案仅在知道文件包含无效字符时才有用。我们的软件只需要处理Unicode,UTF8和系统默认编码,所以真正的问题是加载没有编码参数的文件。除非文件正确检测为UTF8 BOM包含无效的UTF8序列,否则VCL将在所有情况下“工作”。这样的文件最终被加载为空。 – frogb 2014-07-30 08:49:29

+1

正确 - 此解决方案假定您知道编码为UTF-8,因此如果您尝试通过BOM或内容嗅探编码,则此方法不适用。 – 2014-07-31 06:53:31

0

您的“全局”方法并非真正的全局方法 - 它依赖于所有代码只会使用同一个实例TUTF8Encoding的假设。您在黑客入侵标志字段的同一个实例。

但是,如果一个获得TUTF8Encoding对象(一个或多个)通过其他手段比TEncoding.GetUTF8,例如在XE2另一种方法它不会工作 - TEncoding.GetEncoding(CP_UTF8) - 将创建的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内存区域移除保护,然后进行恢复。

例子来学习从

或者你可以尝试使用德尔福弯路库,希望它可以修补虚拟构造函数。然后......为了这个单一的目标,使用那个相当复杂的库可能是一个矫枉过正的问题。

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


然后,如果您编译程序作为single monolithic EXE,而不使用运行时BPL模块,你才可以在SysUtils.pas来源复制到您的应用程序文件夹,然后以包括本地复制到项目明确。

How to patch a method in Classes.pas

有你认为合适的来源,你会改变非常TUTF8Encoding执行和Delphi会使用它。

如果您的项目将被构建为重用rtlNNN.bpl运行时软件包而不是单片,那么这种大脑致命的简单化(因此 - 同样可靠)方法将不起作用。

+0

感谢您的建议,我希望对其他人有用,但不幸的是他们没有添加任何我需要的东西。正如我在第一次提出这个问题时所说的,我从来不需要编码,比如你创建的MyEncoding。我的问题的核心是自动检测传递到我的应用程序,这是不受我控制的文件的编码。所以我从不需要提供编码。我只需要避免一个异常,或一个空文件,当一个文件的UTF8无效被呈现和阅读。我接受的解决方案对我来说工作了很多年,这就是为什么我如此标记它。 – frogb 2017-01-17 23:14:42

+0

您没有完整地修补自动检测,但只有一条路径。根据两个预感,您正在构建您的安全性:任何库都不会使用任何其他方法来获得标准的'TUTF8Encoding'对象,并且任何库都不会''销毁'您修补的单个'TUTF8Encoding'对象。两者都是摇摇欲坠的理由,他们可能为99%的案件工作,然后给你1%的错误。而且由于你错误地认为你“修补了内置的UTF8检测”(你只是部分地做了这个),所以你永远不会有那么难以忽视这些 – 2017-01-18 09:45:27

+0

的来源,因为你创建的MyEncoding只是一个蹦床装置,使德尔福建立一个功能,然后注入标准的TUTF8Encoding在永久的基础上。你永远不会为自己使用这个类。你错过了这一点 - 它应该是'TUTF8Encoding'类需要补丁,而不是它的实例。 'MyEncoding'类不是在@Marc Durdin答案中使用的类,你永远不会实例化它,它只是固定代码的一个捐助者,用于修补内置类。 – 2017-01-18 09:48:36

相关问题