Blog. Just Blog

Как преобразовать кириллическую строку из UTF-8 в cp1251

Версия для печати Добавить в Избранное Отправить на E-Mail | Категория: Образ мышления: Assembler | Автор: ManHunter
Как преобразовать кириллическую строку из UTF-8 в cp1251
Как преобразовать кириллическую строку из UTF-8 в cp1251

При разработке одной программы мне понадобилось преобразовать строки на русском языке из формата UTF-8 в формат cp1251. Внезапно выяснилось, что никакие средства WinAPI не позволяются выполнить эту операцию "одной строкой". Пришлось рассматривать даже варианты с табличным преобразованием, но потом нашлось более простое решение задачи. Алгоритм преобразования получился необычный, но зато гарантированно рабочий. Может быть это поможет сохранить время и нервы кому-нибудь еще.

Для начала вспомогательная функция, которая проверяет, является ли строка кириллической строкой в формате UTF-8. В ней должны быть только однобайтовые символы из первой половины таблицы ASCII и двухбайтовые символы, соответствующие русским буквам. Нулевой символ - признак окончания строки, длина строки значения не имеет.
  1. ;--------------------------------------------------------
  2. ; Проверка строки на соответствие формату
  3. ; кириллического UTF-8
  4. ;--------------------------------------------------------
  5. ; Символы [0x00-0x7F] или двухсимвольные конструкции
  6. ; вида 0xD0[0x81|0x90-0xBF] или 0xD1[0x91|0x80-0x8F]
  7. ;--------------------------------------------------------
  8. ; На выходе:
  9. ;    EAX = 1 - строка соответствует UTF-8
  10. ;    EAX = 0 - строка не соответствует формату
  11. ;--------------------------------------------------------
  12. proc    is_utf8 tstr:DWORD
  13.         push    esi ebx
  14.         mov     esi,[tstr]
  15.  
  16.         ; По умолчанию строка соответствует формату
  17.         mov     ebx,1
  18. .loc_scan:
  19.         lodsb
  20.         ; Окончание строки?
  21.         or      al,al
  22.         jz      .loc_ret
  23.  
  24.         ; Проверка символов [0x00-0x7F]
  25.         cmp     al,07Fh
  26.         jbe     .loc_scan
  27.  
  28.         ; Проверка двухсимвольной конструкции
  29.         ; 0xD0[0x81|0x90-0xBF]
  30.         cmp     al,0D0h
  31.         jne     @f
  32.         lodsb
  33.         cmp     al,81h
  34.         je      .loc_scan
  35.         cmp     al,90h
  36.         jb      .loc_fail
  37.         cmp     al,0BFh
  38.         ja      .loc_fail
  39.         jmp     .loc_scan
  40. @@:
  41.         ; Проверка двухсимвольной конструкции
  42.         ; 0xD1[0x91|0x80-0x8F]
  43.         cmp     al,0D1h
  44.         jne     .loc_fail
  45.         lodsb
  46.         cmp     al,91h
  47.         je      .loc_scan
  48.         cmp     al,80h
  49.         jb      .loc_fail
  50.         cmp     al,8Fh
  51.         jbe     .loc_scan
  52.  
  53. .loc_fail:
  54.         ; Строка не соответствует формату
  55.         xor     ebx,ebx
  56.  
  57. .loc_ret:
  58.         mov     eax,ebx
  59.         pop     ebx esi
  60.         ret
  61. endp
И еще одна аналогичная функция для наиболее точной и глубокой проверки соответствия строки формату UTF-8. Корректно обрабатывает даже битые строки, которые теоретически может пропустить предыдущая функция.
  1. ;--------------------------------------------------------
  2. ; Проверка строки на соответствие формату UTF-8
  3. ;--------------------------------------------------------
  4. ; На входе:
  5. ;    lpStr  - указатель на проверяемую строку
  6. ;--------------------------------------------------------
  7. ; На выходе:
  8. ;    EAX = 1 - строка соответствует UTF-8
  9. ;    EAX = 0 - строка не соответствует формату
  10. ;--------------------------------------------------------
  11. proc is_valid_utf8 lpStr:DWORD
  12.         locals
  13.                 cp  dd ?
  14.                 num dd ?
  15.         endl
  16.  
  17.         push    esi ebx ecx
  18.         mov     esi,[lpStr]
  19.  
  20.         ; По умолчанию строка соответствует формату
  21.         mov     ebx,1
  22. .loc_scan:
  23.         lodsb
  24.         ; Окончание строки?
  25.         or      al,al
  26.         jz      .loc_ret
  27.  
  28.         ; U+0000 to U+007F
  29.         mov     ah,al
  30.         and     ah,0x80
  31.         or      ah,ah
  32.         jnz     @f
  33.  
  34.         and     al,0x7F
  35.         movzx   eax,al
  36.         mov     [cp],eax
  37.         mov     [num],1
  38.         jmp     .loc_check
  39. @@:
  40.         ; U+0080 to U+07FF
  41.         mov     ah,al
  42.         and     ah,0xE0
  43.         cmp     ah,0xC0
  44.         jne     @f
  45.  
  46.         and     al,0x1F
  47.         movzx   eax,al
  48.         mov     [cp],eax
  49.         mov     [num],2
  50.         jmp     .loc_check
  51. @@:
  52.         ; U+0800 to U+FFFF
  53.         mov     ah,al
  54.         and     ah,0xF0
  55.         cmp     ah,0xE0
  56.         jne     @f
  57.  
  58.         and     al,0x0F
  59.         movzx   eax,al
  60.         mov     [cp],eax
  61.         mov     [num],3
  62.         jmp     .loc_check
  63. @@:
  64.         ; U+10000 to U+10FFFF
  65.         mov     ah,al
  66.         and     ah,0xF8
  67.         cmp     ah,0xF0
  68.         jne     @f
  69.  
  70.         and     al,0x07
  71.         movzx   eax,al
  72.         mov     [cp],eax
  73.         mov     [num],4
  74.         jmp     .loc_check
  75. @@:
  76.         jmp     .loc_fail
  77. .loc_check:
  78.         mov     ecx,[num]
  79. @@:
  80.         dec     ecx
  81.         or      ecx,ecx
  82.         jz      @f
  83.  
  84.         lodsb
  85.         mov     ah,al
  86.         and     ah,0xC0
  87.         cmp     ah,0x80
  88.         jne     .loc_fail
  89.  
  90.         shl     [cp],6
  91.         and     al,0x3F
  92.         movzx   eax,al
  93.         or      [cp],eax
  94.         jmp     @b
  95. @@:
  96.         ; (cp > 0x10FFFF)
  97.         cmp     [cp],0x10FFFF
  98.         ja      .loc_fail
  99.  
  100.         ; (cp >= 0xD800) && (cp <= 0xDFFF)
  101.         cmp     [cp],0xD800
  102.         jb      @f
  103.         cmp     [cp],0xDFFF
  104.         jbe     .loc_fail
  105. @@:
  106.         ; (cp <= 0x007F) && (num != 1)
  107.         cmp     [cp],0x007F
  108.         ja      @f
  109.         cmp     [num],1
  110.         jne     .loc_fail
  111. @@:
  112.         ; (cp >= 0x0080) && (cp <= 0x07FF) && (num != 2)
  113.         cmp     [cp],0x0080
  114.         jb      @f
  115.         cmp     [cp],0x07FF
  116.         ja      @f
  117.         cmp     [num],2
  118.         jne     .loc_fail
  119. @@:
  120.         ; (cp >= 0x0800) && (cp <= 0xFFFF) && (num != 3)
  121.         cmp     [cp],0x0800
  122.         jb      @f
  123.         cmp     [cp],0xFFFF
  124.         ja      @f
  125.         cmp     [num],3
  126.         jne     .loc_fail
  127. @@:
  128.         ; (cp >= 0x10000) && (cp <= 0x1FFFFF) && (num != 4)
  129.         cmp     [cp],0x10000
  130.         jb      @f
  131.         cmp     [cp],0x1FFFFF
  132.         ja      @f
  133.         cmp     [num],4
  134.         jne     .loc_fail
  135. @@:
  136.         jmp     .loc_scan
  137.  
  138. .loc_fail:
  139.         ; Строка не соответствует формату
  140.         xor     ebx,ebx
  141.  
  142. .loc_ret:
  143.         mov     eax,ebx
  144.         pop     ecx ebx esi
  145.         ret
  146. endp
Чтобы не плодить сущности, покажу еще маленький код для определения, что текст записан в кодировке Unicode. Для этого используется штатная функция IsTextUnicode. Про совместимость переживать не стоит, она доступна аж с Windows 2000.
  1.         IS_TEXT_UNICODE_UNICODE_MASK = 0x000F
  2.         ; Тестируемый формат строки
  3.         mov     [tmp],IS_TEXT_UNICODE_UNICODE_MASK
  4.         invoke  IsTextUnicode,test_string,string_len,tmp
  5.         ; EAX = 1 - строка соответствует формату
  6.         ; EAX = 0 - проверка не пройдена
Теперь пример кода для преобразования строки из UTF-8 в cp1251. Фишка заключается в том, что здесь используется промежуточный шаг с преобразованием UTF-8 в UTF-16 через функцию MultiByteToWideChar, и только после этого полученная промежуточная строка из UTF-16 преобразуется в cp1251 при помощи функции WideCharToMultiByte. Извратно, конечно, но зато работает.
  1.         ; Получить нужную длину строки для конвертирования
  2.         invoke  MultiByteToWideChar,CP_UTF8,0,str8,-1,0,0
  3.         or      eax,eax
  4.         ; Конвертировать строку невозможно
  5.         jz      can_not_convert
  6.         ; Промежуточное конвертирование UTF-8 -> UTF-16
  7.         invoke  MultiByteToWideChar,CP_UTF8,0,str8,-1,buff16,eax
  8.         ; Получить нужную длину строки для конвертирования
  9.         invoke  WideCharToMultiByte,1251,0,buff16,-1,0,0,0,0
  10.         or      eax,eax
  11.         ; Конвертировать строку невозможно
  12.         jz      can_not_convert
  13.         ; Финальное конвертирование UTF-16 -> cp1251
  14.         invoke  WideCharToMultiByte,1251,0,buff16,-1,str1251,eax,0,0
  15.  
  16. successfully_converted:
  17.         ; str1251 = отконвертированная строка
  18.         ...
  19.  
  20. can_not_convert:
  21.         ; Конвертировать строку невозможно
  22.         ...
На каждом этапе конвертирования выполняется дополнительная проверка на возможность преобразования. Если расчетная длина строки получается нулевая, значит преобразовать строку по какой-то причине нельзя. Это не отменяет использование описанной выше функции проверки на соответствие строки формату UTF-8.

Корме описанных выше, в системе есть еще несколько функций для преобразования строк из одной кодировки в другую. Например, для преобразования из UTF-8 в UTF-16 можно воспользоваться следующим кодом. Тут используется недокументированная функция SHAnsiToUnicodeCP из библиотеки shlwapi.dll. По имени эта функция не экспортируется, надо получать ее адрес вручную по ординалу 216:
  1.         ; Загрузить библиотеку shlwapi.dll
  2.         invoke  LoadLibrary,szLib
  3.         ; Получить адрес функции SHAnsiToUnicodeCP
  4.         invoke  GetProcAddress,eax,216
  5.         stdcall eax,CP_UTF8,str8,buff16,255
Для преобразования строки из UTF-16 в cp1251 также есть альтернативные функции, например, RtlUnicodeStringToAnsiString. Она используется следующим образом:
  1. struct ANSI_STRING
  2.         Length dw ?
  3.         MaximumLength dw ?
  4.         Buffer dd ?
  5. ends
  6.  
  7. struct UNICODE_STRING
  8.         Length dw ?
  9.         MaximumLength dw ?
  10.         Buffer dd ?
  11. ends
  12.  
  13. ansi  ANSI_STRING
  14. unic  UNICODE_STRING
  15.  
  16.         invoke  RtlInitAnsiString,ansi,str1251
  17.         invoke  RtlInitUnicodeString,unic,str16
  18.         mov     [ansi.MaximumLength],255
  19.         invoke  RtlUnicodeStringToAnsiString,ansi,unic,FALSE
Или функция SHUnicodeToAnsi, более короткая для записи и гораздо более удобная в использовании.
  1.         invoke  SHUnicodeToAnsi,str16,str1251,100h
Или недокументированная функция SHUnicodeToAnsiCP, которая загружается по ординалу 218 из библиотеки shlwapi.dll.
  1.         ; Загрузить библиотеку shlwapi.dll
  2.         invoke  LoadLibrary,szLib
  3.         ; Получить адрес функции SHUnicodeToAnsiCP
  4.         invoke  GetProcAddress,eax,218
  5.         stdcall eax,1251,str16,str1251,255
В приложении пример программы с исходным текстом, которая конвертирует строки из кодировки UTF-8 в cp1251.

Пример программы с исходным текстом (FASM)Пример программы с исходным текстом (FASM)

UTF8.cp1251.Demo.zip (2,256 bytes)


Поделиться ссылкой ВКонтакте
Просмотров: 9612 | Комментариев: 10

Внимание! Статья опубликована больше года назад, информация могла устареть!

Комментарии

Отзывы посетителей сайта о статье
ManHunter (20.05.2023 в 12:37):
Добавил примеры альтенативных функций для преобразования строк из одной кодировки в другую.
ManHunter (20.09.2022 в 18:29):
Добавил более навороченную функцию проверки на соответствие строки формату UTF-8
ManHunter (31.07.2016 в 17:44):
Примеры приведены. Берешь и используешь в своей программе.
Денис (31.07.2016 в 12:04):
Как это можно использовать для перекодировки?
ManHunter (03.01.2015 в 10:15):
*facepalm*
Igor K, не пиши больше ничего, не надо
Igor K (03.01.2015 в 09:24):
Перевод делается с помощью Notepad++
brute (31.10.2014 в 07:11):
morgot, я имел в виду команды SSE и другие, которые за один такт работают со строками по 128бит (или более).
morgot (29.10.2014 в 17:03):
brute, какой это "32 разрядный mov"? На 64 битах этой команды нет, по вашему? Про функцию вообще промолчу, могу сказать только то, что ваш образ мышления уж явно не Ассемблер.

ManHunter, спасибо, пригодится.
ManHunter (28.10.2014 в 10:35):
Все равно в нормальном софте придется делать обратную совместимость как минимум до WinXP
brute (28.10.2014 в 06:34):
Круто! но я бы искал какую-нибудь сишную библиотеку, чтобы заинклудить нужную функцию. Она наверняка эффективнее, чем "старый" 32 разнрядный "mov". П.С. слышал, что в W7 и W8 появилось много новых api-функций, в том числе есть и новые строковые..

Добавить комментарий

Заполните форму для добавления комментария
Имя*:
Текст комментария (не более 2000 символов)*:

*Все поля обязательны для заполнения.
Комментарии, содержащие рекламу, ненормативную лексику, оскорбления и т.п., а также флуд и сообщения не по теме, будут удаляться. Нарушителям может быть заблокирован доступ к сайту.
Наверх
Powered by PCL's Speckled Band Engine 0.2 RC3
© ManHunter / PCL, 2008-2024
При использовании материалов ссылка на сайт обязательна
Время генерации: 0.08 сек. / MySQL: 2 (0.0064 сек.) / Память: 4.5 Mb
Наверх