Вся правда про UTF-8 прапор

Найпоширеніша помилка полягає в тому, що рядки символів, на відміну від рядків байтів, мають UTF-8 прапор встановленим. Багато хто здогадується, що якщо дані є ASCII-7-bit, то UTF-8 прапор просто не важливий.
Однак, насправді він може бути встановлений або скинутий, як і у символів, так і абсолютно довільних бінарних даних.
Ви можете мати Unicode strings з тим, що flag set, with that flag clear, and you can have binary data with that flag set and that flag clear. Інші можливості exist, too.
Розглянемо випадок, коли ASCII-7bit дані мають UTF-8 прапор установленим.
Цей код виводить "UTF-8 flag set!". Тобто ASCII-7bit рядок отримав цей прапор після того, як операція split розділила Unicode рядок (з UTF-8 прапором) на частини. Можна сказати, що програміст не контролює, чи буде у його ASCII даних UTF-8 прапор чи ні, це залежить від того, звідки і як отримані дані, і від того, які дані були поруч із ними.
Той самий ефект виходить, якщо декодувати ASCII-7bit байти в ASCII-7bit символи за допомогою Encode::decode()
Тобто. перекодування туди-назад не змінює дані (це очікувано), але встановлює UTF-8 прапор. (втім, така поведінка decode() суперечить його власної документації, яка, у свою чергу, суперечить ідеї, що жодної документації та гарантій щодо utf-8 прапора в ASCII даних бути не повинно)
Пояснити ж причини виникнення UTF-8 прапора можна міркуваннями ефективності. Занадто накладно після split аналізувати рядок, щоб зрозуміти, складається вона тільки з ASCII символів, і чи можна скинути прапор.
Така поведінка UTF-8 прапора схожа на вірус - він заражає всі дані, з якими стикається.
Розглянемо випадок, коли не ASCII, Unicodeсимволи немає UTF-8 прапора.
Тобто, виклик функції стороннього модуля скинув UTF-8 прапор. При цьому рядки з прапором і без виявилися повністю ідентичними. Таке може статися лише з символами > 127 та
Перетворюються на місце міжнародного представництва з string від UTF-X до подібного octet sequence в природному усвідомленні (Latin-1 або EBCDIC). Логічний характер sequence йогоself is unchanged.
В принципі модуль Digest::SHA документує таку свою поведінку, хоча не зобов'язаний:
Будь-яка думка, що digest routines silently convert UTF-8 введення в його equivalent byte sequence в природному encoding (cf. utf8::downgrade). Це side effect influences on the way Perl stores the data internally, but otherwise leaves the actual value of the data intact.
У загальному випадку будь-яка 3-rd party функція може зробити downgrade рядка, не повідомляючи в цьому документації (або, наприклад, робити його тільки іноді).
Розглянемо випадок коли абсолютно довільні, бінарні дані мають UTF-8 прапор.
В результаті виходить, що бінарні дані, після конкатенації з ASCII рядком, збільшили свій внутрішній розмір в байтах (але не в символах) з 4 до 7, але тільки у випадку, якщо UTF-8 прапор у ASCII був встановлено, нічого не значущий .
Однак, при порівнянні цих даних між собою, вони ідентичні, також, при виведенні у файл обох рядків, навіть без вказівки кодування, файли теж ідентичними.
Таким чином, бінарні дані можуть збільшитися в розмірі і отримати UTF-8 прапор, при цьому ніякого бага немає, всі вбудовані функції Perl обробляють їх так само, як якщо б прапора не було (якщо є винятки, то баг в них).
Будь-який інший perl код також повинен обробляти такі дані без помилок (якщо він ненамагається аналізувати внутрішню струкутуру рядка, чи хоча б аналізує її правильно)
Насправді те, що трапилося з бінарними даними, є аналогом операції utf8::upgrade . Дані були інтерпретовані як Latin-1, конвертовані в UTF-8 і встановлено UTF-8 прапор. Це операція протилежнаutf8::downgrade, описаної вище.utf8::downgradeможе виконуватися лише з Latin-1 символами. Аutf8::upgradeможе проводитися з будь-якими байтами (бо будь-якому байту відповідає символ з Latin-1).
Це може бути важливо, якщо у пам'яті великий обсяг бінарних даних. Зовсім не здорово, якщо 400-мегабайтний блоб, раптом перетворюється на 700-мегабайтний, тільки тому, що ви додали туди один ASCII-7bit байт з UTF-8 прапором. Хороший вихід із ситуації тут — unit тести або runtime assertions з перевіркою прапора UTF-8.
Загалом, неможливо відрізнити байти від символів
Розглянемо завдання: написати функцію, на вхід якої подаватиметься XML, якщо XML є байтами, подивитися кодування в тезі «xml» і перекодувати їх у символи. Якщо вона є символами, нічого не робити.
Таку функцію реалізувати не вдасться. Наприклад, для рядка символів Hello, München, функція не зможе відрізнити символи це, або байти в кодуванні CP1251, або в KOI8-R (у разі якщо рядок виявиться downgraded, а це програміст в загальному випадку не контролює).
Для символів > 255, UTF-8 прапор завжди встановлений (з ними не можна зробитиutf8::downgrade). Для символів із кодом
Як можна визначити, якщо string є текстом string або binary string?
Ви не можете. Деякий використовує UTF8 flag для цього, але те, що misuse, і робить добре виконані modules як Data::Dumper look bad. The flag is useless forцей purpose, тому що це off, коли 8 біт передач (відповідно до ISO-8859-1) використовується за межами string.
This is something you, the programmer, has to keep track of; sorry. Ви робите, що adopting a kind of «Hungarian notation» help with this.
Якщо вам все ж таки потрібно це зробити, можна створити свій клас, який буде містити рядок байтів або символів, і прапор, що показує що це (той самий трюк підійде для email vs ім'я файлу vs ім'я людини).
Wide characters не видається для символів з Latin-1
Приклад прикладу видає warningWide characters in printтільки якщо ми друкуємо $s2
Якщо ми друкуємо $s1, Perl конвертує символ Unicde µ (U+00DF, UTF-8 xC3xF9) в байт xDF і намагається вивести його на екран. Така ж поведінка справедлива для всіх функцій, які приймають байти, а не символи (print, syswrite без вказівки кодування, контрольні суми SHA, MD5, CRC32, MIME::Base64).
Вірусний downgrade
На початку статті було описано «вірусну» поведінку UTF-8 біта у символів ASCII (віруснийutf8::upgrade). Тепер розглянемо "вірусний" скидання UTF-8 біта у Latin-1 символів(utf8::downgrade).
Уявімо, що ми пишемо функцію, яка визначена тільки над байтами, а не над символами, добрим прикладом є hash-функції, шифрування, архівування, Mime:: Base64 і т.д.
1. Якщо неможливо відрізнити бінарні дані від символів, ви повинні розглядати вхідні дані як байти. 2. Байти можуть матиupgradeформу (бо з UTF-8 прапором). Результат має бути такий самий як уdowngradeформи.
Отже, потрібно зробитиutf8::downgradeі видати помилку, якщо це не вийде.
Для алгоритмів типу хеш-функцій характерна турбота про продуктивність. Робитидругу копію даних у пам'яті не ефективно, так що, в більшості випадків, функція модифікує параметр, що передається їй.
Як, напевно, багато хто знає, у Perl всі параметри передаються за посиланням, але зазвичай використовуються за значенням.
Таким чином, при створенні коду, який працює точно у відповідність до специфікації Perl, створюється код, що непомітно робитьutf8::downgradeнад фактичними параметрами, незалежно від волі викликає, тим самим, можливо, створивши баг в якому іншому місці, яке неправильно обробляло рядки, і до цього моменту працювало добре.
Для імен файлів все це не працює
Функції, які приймають імена файлів як аргументи (open, файлові тести-X), а також, які повертають імена файлів (readdir), не підкоряються цим правилам (це зазначено у документації).
Вони просто інтерпретують ім'я файлу, як воно є у пам'яті.
Алгоритм їхньої роботи можна описати так:
Для цього є кілька причин:
1. У багатьох POSIX системах ( Linux / *BSD ), на багатьох файлових системах, ім'ям файлу може бути довільна послідовність байтів, яка не обов'язково є послідовністю символів у будь-якому кодуванні. 2. Немає способу визначити кодування файлової системи. 3. На машині може бути кілька файлових систем з різним кодуванням 4. Не можна спиратися на припущення, що кодування імен файлів збігатиметься з кодуванням локалі. 5. Має бути сумісність із старим кодом.
У підсумку програміст повинен сам визначати кодування та повідомляти його інтерпретатору, але API для цього ще не зробили.
Модифікуємо наш приклад, де ми випадково наткнулися на downgrade рядка символів.
тобто. рядки s1 і s2 збігаються,але вказують різні файли, якщо вивезенняsha1_hexприбрати, то однакові файли.
На ці ж граблі можна натрапити, звертаючись до будь-яких модулів, що працюють з файлами (наприкладFile::Find)
Коли ще це не працює
У модулі Encode є функція decode_utf8 документована як:
Equivalent to $string = decode(«utf8», $octets[, CHECK])
Але насправді, якщо $octets встановлений прапор UTF-8, функція просто повертає їх незмінними (хоча повинна спробувати зробитиutf8::downgradeі працювати з ними, як з бінарними даними, а якщоdowngradeне вийде, видати помилкуWide characters).
Цей баг був помічений (RT#61671 RT#87267) відразу як з'явився — у 2010 році.
The Unicode Bug
Уdowngradedформі Latin-1 не можна відрізнити від байтів, отже, у цій формі, погано працюють деякі метасимволи в регулярних виразах, функціїuc,lc,quotemeta.
Воркераунд -utf8::downgrade, або, в нових версіях Perl - деякі директиви, які дозволяють зробити цю поведінку консистентною.
Що ж робити з цим?
1. Не користуйтеся (якщо ви точно не знаєте, що робите) такими функціями:utf8::is_utf8,Encode::_utf8_on,Encode::_utf8_on, та всіма функціями з модуляbytes(документація до всіх цих функцій не рекомендує їх використання, крім як для налагодження)
2. Користуйтесяutf8::upgrade,utf8::downgrade, щоразу, коли цього вимагає специфікація Perl
3. Для конвертації із символів у байти використовуйтеEncode::encode,Encode::decode
3. Якщо використовуєте чужий код, що порушує ці правила, перевірте його на наявність багів, застосовуйте workaroundы.
4. Прироботи з іменами файлів, або доведеться використовувати wrapper над усіма функціями, або, за допомогою тестів, переконатися, що внутрішнє представлення імен файлів не змінюється в процесі роботи коду.
Є кілька прикладів, коли порушення цих правил мені видалося виправданим.
(скидає UTF-8 прапор для ASCII-7bit тексту (тим самим вдається досягти 30% збільшення продуктивності регекспів, у всіх Perl, крім 5.19))
(Повертає TRUE, якщо у рядка встановлено UTF-8 прапор, і при цьому він не є ASCII-7bit. Може використовуватися в unit тестах, щоб переконатися, що ваші 400 мегабайт бінарних даних не перетворюються на 700)
Є ще варіант нічого не робити. Чесно кажучи, пройде досить багато часу, перш ніж ви натрапите на якийсь баг (але, на той момент буде вже пізно). Цей варіант не рекомендується для розробників бібліотек.