2013-05-13 67 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

相關問題