我发现(很难)如果一个文件有一个有效的UTF-8 BOM但包含任何无效的UTF8编码,并且被任何Delphi(2009+)编码启用的方法(如LoadFromFile)读取,那么结果是一个完全空的文件,没有错误指示。在我的几个应用程序中,我宁愿丢失一些不良编码,即使在这种情况下我也没有得到任何错误报告。
调试显示MultiByteToWideChar被调用两次,首先获取输出缓冲区大小,然后进行转换。但是TEncoding.UTF8包含这些调用的私有FMBToWCharFlags值,并使用MB_ERR_INVALID_CHARS值初始化。因此,获取charcount的调用返回0并且加载的文件完全为空。在没有标志的情况下调用此API将"默默地删除非法代码点"。
我的问题是如何最好地编织Encoding区域中的类的嵌套来解决这个私有值的事实(并且需要,因为它是所有线程的类var)。我想我可以使用Marco Cantu的Delphi 2009书中的指南添加自定义UTF8编码。如果MultiByteToWideChar返回编码错误,在没有标志的情况下再次调用它后,它可以选择性地引发异常。但这并没有解决如何使用我的自定义编码而不是Tencoding.UTF8的问题。
如果我可以在初始化时将其设置为应用程序的默认值,也许通过实际修改Tencoding.UFT8的类var,这可能就足够了。
当然,我需要一个解决方案,而不是等待提交质量控制报告,要求更强大的设计,接受它,并看到它改变。
任何想法都会非常受欢迎。并且有人可以确认这仍然是XE4的一个问题,我还没有安装?
-
如果您有答案,请将其作为答案发布,而不是编辑问题。 否则问题将永远保持开放,没有答案。
当我第一次更新Indy以支持TEncoding时,我遇到了MB_ERR_INVALID_CHARS问题,并最终为UTF-8处理实现了一个自定义的TEncoding派生类,以避免指定MB_ERR_INVALID_CHARS。我没想过要使用类助手。
但是,这个问题不仅限于UTF-8。任何TEncoding类的任何解码失败都将导致空白结果,而不是引发异常。当大多数RTL / VCL使用异常时,为什么Embarcadero选择了这条路线,这超出了我的范围。不提出错误的例外导致Indy中的大量问题必须解决。
-
+1导出您自己的自定义TEncoding显然是您应该做的。
-
TEncoding存在很多设计和实现问题,因此在Indy 10.6中我决定完全删除TEncoding并编写自己的基于接口的框架来替换它。
-
@David:当LoadFromFile检测到BOM时,您将如何使用编码?您是否必须读取前三个字节,然后为您找到的任何UTF8文件传递编码参数?
-
@frogb:是的,你会的。 TEncoding不允许将用户定义的类注册到其默认的BOM处理逻辑中。
-
@remy:谢谢。我会接受你的答案,这对于维持Indy的人来说显然是正确的;但我自己更适合我,更接近我原来的问题。经常发生这种问题,可以帮助您自己找到答案!
-
@RemyLebeau是Indy的一部分还是单独提供?我也想这样做,但最终被推迟到无限。我不明白为什么他们试图模仿基于GC的DotNet类与手动内存控制实现.....偶尔TEncoding.UTF8.Free - 并等待它爆炸......
-
@Arioch'The:我写的代码只适用于Indy。它是IdGlobal单元中的IIdTextEncoding接口和支??持类/例程。
这可以非常简单地完成,至少在Delphi XE5中(没有检查过早期版本)。只是实例化你自己的TUTF8Encoding:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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; |
这里唯一的问题是新实例化的TUTF8Encoding的IsSingleByte属性被错误地设置为False,但此属性当前未在Delphi源中的任何位置使用。
-
不幸的是,只有当您知道文件包含无效字符时,该解决方案才有用。我们的软件只需要处理Unicode,UTF8和系统默认编码,因此真正的问题是加载没有编码参数的文件。然后VCL在所有情况下都"工作",除非正确检测到文件的UTF8 BOM包含无效的UTF8序列。这样的文件最终加载为空。
-
True - 此解决方案假设您知道编码为UTF-8,因此如果您尝试通过BOM或内容嗅探编码,则不适用。
部分解决方法是强制UTF8编码全局禁止MB_ERR_INVALID_CHARS。对我来说,这避免了引发异常的需要,因为我发现它使得MultiByteToWideChar不太"沉默":它实际上插入了$fffd字符(Unicode'替换字符'),然后我可以在这种情况下找到它很重要以下代码执行此操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 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 10.1你可以class helper for Tmbcsencoding public property UnicodeFlags: cardinal read FMBToWCharFlags write FMBToWCharFlags end;然后使用initialization Tencoding.UTF8.UnicodeFlags := 0; end.
-
如果通过除TEncoding.GetUTF8之外的其他方式获得TUTF8Encoding对象,它也将无法工作,例如在XE2 TEncoding.GetEncoding(CP_UTF8)中将创建TUTF8Encoding的新实例而不是本地实例
-
条件编译的目的是使用最初实现的代码帮助程序保留早于柏林的代码的原始发布解决方案。我不确定未来编译器将要做什么,因为即使是ASM解决方案也可能在将来的版本中关闭。
-
正如我在下面解释的那样,接受代码的目的是修复内置的UTF8检测。我没兴趣获得新的编码对象。但无论如何,谢谢。
-
你不能确保你使用的库不能做到这一点。那些"新对象"是完全相同的"内置检测"。更重要的是,如果任何库因任何原因调用标准FreeEncodings方法,则会重新创建该对象
您的"全局"方法并非真正全局 - 它依赖于所有代码仅使用同一个TUTF8Encoding实例的假设。您攻击标志字段的相同实例。
但是,如果通过除TEncoding.GetUTF8以外的其他方式获取TUTF8Encoding对象,则无法工作,例如在XE2中,另一种方法 - TEncoding.GetEncoding(CP_UTF8) - 将创建TUTF8Encoding的新实例,而不是重新使用分享一个。或者某些功能可能直接运行TUTF8Encode.Create。
所以我建议另外两种方法。
修补类实现的方法,有点hacky。为了获得新的"修复"构造函数体,您引入了自己的类。
1 2 3
| type TMyUTF8Encoding = class(TUTF8Encoding)
public constructor Create; override;
end; |
这个构造函数将是TUTF8Encoding.Create()实现的模仿,除了根据需要设置标志(在XE2中它通过调用另一个,继承的Create(x,y,z)来完成,因此你不需要访问私有字段)。
然后你可以修补库存TUTF8Encoding VMT覆盖它的虚拟构造函数到你的新构造函数。
您可以阅读有关"内部格式"等的Delphi文档,以获取VMT布局。您还需要在修补之前调用VirtualProtect(或其他特定于平台的功能)来删除对VMT内存区域的保护,然后再进行恢复。
要学习的例子
-
如何更改外部声明的函数的实现(绕行)
-
https://stackoverflow.com/a/1482802/976391
或者您可以尝试使用Delphi Detours库,希望它可以修补虚拟构造函数。然后......在这里使用那个相当复杂的lib来实现这个单一目标可能是一种过度杀伤力。
在您攻击TUTF8Encoding类之后,请调用TEncoding.FreeEncodings以删除已创建的共享实例(如果有),从而触发重新创建具有您的修改的UTF8实例。
然后,如果您将程序编译为single monolithic EXE,而不使用运行时BPL模块,则只需将SysUtils.pas源复制到应用程序文件夹,然后将该本地副本显式包含在项目中。
如何在Classes.pas中修补方法
你可以在那里更改你认为合适的TUTF8Encoding实现,而Delphi会使用它。
如果您的项目将被构建为重用rtlNNN.bpl运行时包而不是单一的,那么这种大脑致命的简单(因此同样可靠)方法将无法工作。
-
感谢您的建议,我希望这些建议对其他人有用,但不幸的是,他们没有添加任何我需要的建议。正如我在第一次提出这个问题时所说,我从不要求你创建的MyEncoding等编码。我的问题的核心是AUTOMATIC检测传递给我的应用程序的文件的编码,这不受我的控制。所以我从不需要提供编码。当呈现和读取具有无效UTF8的文件时,我只需要避免异常或空文件。我接受的解决方案对我来说已经有好几年了,这就是为什么我如此标记它。
-
您没有完整地修补AUTOMATIC检测,而只修改了许多路径中的一个。您正在建立两个预感的安全性:没有任何库可以使用任何其他方法来获取标准TUTF8Encoding对象,并且任何库都不会Destroy您修补的单个TUTF8Encoding对象。两者都是不稳定的理由,它们可能适用于99%的情况,然后给你1%的错误。并且因为你有一种错误的感觉,你"修补了内置的UTF8检测"(你只是部分地做了),你永远不会有艰难的时刻明确忽视那些来源
-
as the MyEncoding that you create - 只是一个蹦床设备,使Delphi构建一个函数,然后在永久基础上注入标准TUTF8Encoding。你从不使用那个类本身。你错过了这一点 - 它应该是需要修补的TUTF8Encoding类,而不是它的实例。 MyEncoding类不是要在@Marc Durdin中使用的类,你永远不会实例化它,它只是修补内置类的固定代码的捐赠者。
-
再次感谢您的评论。