понедельник, 31 августа 2020 г.

Существует ли кодовая страница, совпадающая с ASCII и позволяющая сделать двустороннюю конвертацию в Unicode?

Это перевод Is there a code page that matches ASCII and can round trip arbitrary bytes through Unicode? Автор: Реймонд Чен.

Вам может понадобится такая кодовая страница, если у вас есть фрагмент двоичных данных с внедрённым текстом ASCII. Вы бы хотели иметь возможность извлекать текст ASCII и даже манипулировать им, но при этом и также обрабатывать части, не относящиеся к ASCII, как загадочные символы, не имеющие смысла. Но вам также нужно иметь возможность преобразовать их обратно в исходные байты.

Например, формат двоичных данных может быть таким: строка ASCII, за которой следует 32-разрядное целое число в формате big-endian. Вы хотите проанализировать строку ASCII, затем взять следующие четыре символа, перевернуть их (преобразовать в little-endian), а затем преобразовать всё вместе обратно в байты, чтобы можно было извлечь целое число (integer).

В современных языках программирования (типа Delphi и C#) есть много удобных средств для управления текстом, но для этого требуются строковые переменные, которые выражаются как последовательность кодов UTF16-LE. Итак, вам нужен способ конвертировать байты в коды UTF16-LE - причём так, что байты меньше 128 сопоставлялись бы с соответствующими символами ASCII, а байты 128 и выше сопоставлялись бы с чем-то обратимым.

К примеру, кодовая страница UTF-8 не подойдёт, потому что в UTF-8 существуют недопустимые последовательности байтов, которые не будут преобразованы в текст (и обратно). Другой вариант, который вы можете выбрать - это кодовая страница 1252, но она также не будет работать, потому что в ней существует несколько неопределенных кодовых позиций (#$81, #$8D, #$8F, #$90, #$9D - прим. пер.). Это означает, что эти байты будут преобразованы в #$FFFD (REPLACEMENT CHARACTER), который является специальным символом Unicode, означающим "Здесь был символ, но я не могу выразить его в Unicode". Обычно этот символ используется для представления ошибок кодирования. (Прим.пер.: или же подобные байты могут быть вообще удалены из результирующей строки Unicode.)

Хотя вы вряд ли будете пробовать, но я также замечу, что двухбайтовые кодовые страницы тоже не будут работать, потому что они берут пары байтов и преобразуют их в Unicode. Это означает, что изменение порядка символов Unicode (для нашей задачи преобразования endian) не позволит сделать обратное преобразование.

Хорошо, я перейду сразу к делу. Кодовая страница, которую я использую для такого рода вещей - это кодовая страница 437 (прим. пер.: DOSLatinUS - кодовая страница, использовавшаяся в первоначальной версии IBM PC 1981 года). Каждый байт в ней определяется и сопоставляется с уникальной кодовой точкой Unicode, и она совпадает с ASCII для первых 128 значений.
uses
  StrUtils; // для ReverseString

procedure TForm1.Button1Click(Sender: TObject);
type
  CP437String = type AnsiString(437);
var
  CP437: CP437String;
  UStr: String;
  X: Integer; // = UnicodeString
begin
  // Заполнили строку
  // (в реальной программе вы бы загружали строку из потока данных)
  SetLength(CP437, 256);
  for X := 0 to 255 do
    CP437[X + 1] := AnsiChar(X);

  // Преобразовали CP437 в Unicode
  UStr := String(CP437);

  // Меняем порядок _символов_
  UStr := ReverseString(UStr);

  // Преобразовали Unicode обратно в CP437
  CP437 := CP437String(UStr);

  // Проверяем обратное преобразование
  // Должен быть изменён порядок _байт_
  for X := 0 to 255 do
    if Ord(CP437[X + 1]) <> 255 - X then
      raise Exception.CreateFmt('Ошибка преобразования для #%d', [255 - X]);

  // Сообщение должно быть показано
  ShowMessage('Всё верно');
end;
Я взял все байты 0 до 255 и преобразовал их в строку с помощью кодовой страницы 437. Потом строка переворачивается и конвертируется обратно в байты. После чего проверяем, что результирующие байты также меняются местами.

4 комментария:

  1. CP437String = type AnsiString;
    CP437String = type AnsiString(866);
    CP437String = type AnsiString(874);
    CP437String = type AnsiString(1250);
    CP437String = type AnsiString(1251);
    CP437String = type AnsiString(1252);
    CP437String = type AnsiString(1253);
    CP437String = type AnsiString(1254);
    CP437String = type AnsiString(1255);
    CP437String = type AnsiString(1256);
    CP437String = type AnsiString(1257);
    CP437String = type AnsiString(1258);

    Все это прекрасно работает в приведенном вами примере...

    ОтветитьУдалить
    Ответы
    1. Да, это называется "надеяться на детали реализации".

      Это работает сейчас, потому что реализация автоматического конвертирования строк в RTL вызывает MultiByteToWideChar, который выполняет стратегию "best fit": https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1252.txt - т.е. оставляет "неопределённые" точки без изменений.

      Best-fit стратегии не документированы (в частности, о них нет упоминания в документации на MultiByteToWideChar), и полагаться на них не стоит.

      Это особенно верно, если вы собираетесь передавать строки между разными реализациями. Например, DLL может использовать .NET или ICU - и у них может быть иное представление, как следует поступать с неопределёнными кодовыми позициями. Или даже если ваш код будет потом перекомпилироваться под другую платформу (Mac, iOS, Android, Linux) - там правила преобразования тоже могут быть иными.

      К примеру, #$0081 - это управляющий символ High Octet Preset, #$008D - Reverse Line Feed, и т.д. Т.е. какая-то реализация может не посчитать правильным поведение Windows по внедрению в строку ранее отсутствующих в ней символов.

      Поэтому, лучше использовать кодовою страницу с полностью определённым двусторонним преобразованием. Но она, конечно, не единственная.

      Удалить
  2. Я не понимаю исходной проблемы. Разве не подойдёт любая однобайтовая кодировка, являющаяся надмножеством 7-битной ASCII и у которой определены все 256 символов? Но ведь подавляющее большинство кодировок удовлетворяют (наверное) этим условиям, те же кириллические 866 и 1251 тоже прекрасно сработают и переживут преобразование в Юникод и назад. Что такого особенного в 437?

    ОтветитьУдалить
    Ответы
    1. Да, подойдёт. Просто надо учитывать, что это у нас 1251 - а оригинальный пост написан для англоязычной аудитории, которая пользуется 1252 и в душе не чает, кто такой этот 1251.

      Никаких особенностей за 437 нет.

      Удалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.