Есть ли простой способ обойти ошибку Delphi utf8-file?

Я обнаружил (трудный путь), что если файл имеет действительную спецификацию UTF-8, но содержит любые недопустимые кодировки UTF8 и считывается с помощью любого из методов с поддержкой кодирования Delphi (2009+), таких как LoadFromFile, то результатом будет полностью пустой файл без указания ошибки. В нескольких моих приложениях я бы предпочел просто потерять несколько неправильных кодировок, даже если в этом случае я не получу отчет об ошибке.

Отладка показывает, что MultiByteToWideChar вызывается дважды, сначала для получения размера выходного буфера, а затем для преобразования. Но TEncoding.UTF8 содержит закрытое значение FMBToWCharFlags для этих вызовов, и это инициализируется значением MB_ERR_INVALID_CHARS. Таким образом, вызов для получения charcount возвращает 0, а загруженный файл полностью пуст. Вызов этого API без флага будет "незаметно бросать незаконные кодовые точки".

Мой вопрос заключается в том, как лучше сплести через гнездо классов в области кодирования, чтобы обойти тот факт, что это личное значение (и должно быть, потому что это класс var для всех потоков). Я думаю, что я мог бы добавить пользовательскую кодировку UTF8, используя руководство в книге Marco Cantu Delphi 2009. И он мог бы при необходимости вызвать исключение, если MultiByteToWideChar вернул ошибку кодирования после повторного вызова без флага. Но это не решает проблему использования моей пользовательской кодировки вместо Tencoding.UTF8.

Если бы я мог просто установить это значение по умолчанию для приложения при инициализации, возможно, фактически изменив класс var для Tencoding.UFT8, этого, вероятно, будет достаточно.

Конечно, мне нужно решение, не дожидаясь появления отчета по контролю качества, требующего более надежного дизайна, принятия его и просмотра его изменения.

Любые идеи были бы очень желанными. И может кто-то подтвердить, что это все еще проблема для XE4, которую я еще не установил?

Ответ 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 Berlin)

Ответ 2

Я столкнулся с проблемой MB_ERR_INVALID_CHARS, когда я впервые обновил Indy для поддержки TEncoding и закончил реализацию пользовательского класса TEncoding -derived для обработки UTF-8, чтобы избежать указания MB_ERR_INVALID_CHARS. Я не думал использовать помощник класса.

Однако этот вопрос не ограничивается только UTF-8. Любой отказ декодирования любого из классов TEncoding приведет к пустующему результату, а не к возникновению исключения. Почему Embarcadero выбрал этот маршрут, когда большая часть RTL/VCL использует исключения вместо этого, находится вне меня. Не возникновение исключения из-за ошибки вызвало значительное количество проблем в Indy, которые должны были быть обработаны.

Ответ 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.

Ответ 4

Ваш "глобальный" подход не является действительно глобальным - он полагается на предположение, что весь код будет использовать только один и тот же экземпляр TUTF8Encoding. Тот же самый экземпляр, где вы взломали флажок.

Но это не сработает, если вы получите TUTF8Encoding объект другими способами, чем TEncoding.GetUTF8, например, в XE2 другой метод - TEncoding.GetEncoding(CP_UTF8) - создаст новый экземпляр TUTF8Encoding используя FUTF8 общий. Или какая-то функция может запускать TUTF8Encode.Create напрямую.

Итак, я бы предложил еще два подхода.

Подход с исправлением реализации класса, несколько взломанный. Вы вводите свой собственный класс ради получения нового тела конструктора "fixes".

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 с вашими изменениями.


Затем, если вы скомпилируете свою программу как single monolithic EXE, не используя модули BPL для выполнения, вы можете просто скопировать источники SysUtils.pas в вашу папку приложений, а затем явно включить эту локальную копию в свой проект.

Как исправить метод в Classes.pas

Там вы изменили бы самую реализацию TUTF8Encoding, как вы считаете нужным в источниках, и Delphi будет использовать ее.

Этот мозговой смертоносный упрощённый (следовательно, не менее надежный) подход не сработает, хотя если ваши проекты будут построены для повторного использования пакета rtlNNN.bpl runtime вместо монолитного.