Вся правда о UTF-8 флаге

  • Tutorial


Распространённое заблуждение состоит в том, что строки символов, в отличие от строк байтов, имеют UTF-8 флаг установленным.
Многие догадываются, что если данные являются ASCII-7-bit, то UTF-8 флаг просто не важен.

Однако, на самом деле, он может быть установлен или сброшен, как и у символов, так и абсолютно произвольных бинарных данных.



Широко известный в Perl комьюнити автор Marc Lehmann делает об этом замечание в документации к модулю JSON::XS

You can have Unicode strings with that flag set, with that flag clear, and you can have binary data with that flag set and that flag clear. Other possibilities exist, too.


Рассмотрим случай, когда ASCII-7bit данные имеют UTF-8 флаг установленным.


use utf8;
use strict;
use warnings;
my $u = "тест"; # unicode строка
my $ascii = "x"; # обычный ASCII символ
my ($ascii_u, undef) = split(/ /, "$ascii $u");
die unless $ascii_u eq "x"; # тот же ASCII символ
print "UTF-8 flag set!" if utf8::is_utf8($ascii_u); # но теперь у него установлен UTF-8 флаг


Этот код выводит «UTF-8 flag set!». То есть ASCII-7bit строка получила этот флаг, после того, как операция split разделила Unicode строку (с UTF-8 флагом) на части. Можно сказать, что программист не контролирует будет ли у его ASCII данных UTF-8 флаг или нет, это зависит от того, откуда и как получены данные, и от того, какие данные были рядом с ними.

Тот же эффект получается, если декодировать ASCII-7bit байты в ASCII-7bit символы с помощью Encode::decode()

use strict;
use warnings;
use Encode;
my $ascii = 'x'; # ASCII символ
my $ascii_u = decode("UTF-8", encode("UTF-8", "$ascii"));
die unless $ascii_u eq "x"; # тот же ASCII символ
print "UTF-8 flag set!" if utf8::is_utf8($ascii_u); # но теперь у него установлен UTF-8 флаг


Т.е. перекодировка туда-обратно не меняет данные (это ожидаемо), но устанавливает UTF-8 флаг.
(впрочем, такое поведение decode() противоречит его собственной документации, которая, в свою очередь, противоречит идее, что никакой документации и гарантий относительно utf-8 флага в ASCII данных быть не должно)

Объяснить же причины появление UTF-8 флага можно соображениями эффективности. Слишком накладно после split анализировать строку, чтобы понять, состоит ли она только из ASCII символов, и можно ли сбросить флаг.

Такое поведение UTF-8 флага похоже на вирус — он заражает все данные с которыми соприкасается.

Рассмотрим случай, когда не-ASCII, Unicode символы не имеют UTF-8 флага.


use strict;
use warnings;
use Digest::SHA qw/sha1_hex/;
use utf8;
my $s = "µ";
my $s1 = $s;
my $s2 = $s;

my $digest = sha1_hex($s2); # попробуйте закомментировать эту строчку

print "utf-8 bit ON (s1)\n" if utf8::is_utf8($s1);
print "utf-8 bit ON (s2)\n" if utf8::is_utf8($s2);

print "s1 and s2 are equal\n" if $s1 eq $s2;


печатает:

utf-8 bit ON (s1)
s1 and s2 are equal


То есть вызов функции стороннего модуля сбросил UTF-8 флаг. При этом, строки с флагом и без, оказались полностью идентичными.
Такое может случится только с символами > 127 и <=255 (т.е. Latin-1).

На самом деле, со строкой $s2 произошла операция utf8::downgrade

Эта функция описана в документации, как меняющая внутреннее представление строки:

Converts in-place the internal representation of the string from UTF-X to the equivalent octet sequence in the native encoding (Latin-1 or EBCDIC). The logical character sequence itself is unchanged.


В принципе модуль Digest::SHA документирует такое своё поведение, хотя не обязан:

Be aware that the digest routines silently convert UTF-8 input into its
equivalent byte sequence in the native encoding (cf. utf8::downgrade). This
side effect influences only the way Perl stores the data internally, but
otherwise leaves the actual value of the data intact.


В общем случае любая 3-rd party функция может сделать downgrade строки, не сообщая в этом в документации (или, например, делать его только иногда).

Рассмотрим случай, когда абсолютно произвольные, бинарные данные имеют UTF-8 флаг.


use utf8;
use strict;
use warnings;

# нам нужен bytes::length для отладки, ставим '()' чтобы bytes не влияло на ход программы
use bytes ();

my $u = "тест"; # не ASCII строка

# байты, не символы
my $bin = "\xf1\xf2\xf3";

## опять получает ASCII строку с UTF-8 флагом
my $ascii = "x"; # обычный ASCII симовл
my ($ascii_u, undef) = split(/ /, "$ascii $u");
die unless $ascii_u eq "x"; # тот же ASCII символ
die unless utf8::is_utf8($ascii_u); # но теперь у него установлен UTF-8 флаг
## //

print "original bin length:\t";
print length($bin) . "\t" . bytes::length($bin) ."\n";

my $bin_a = $bin.$ascii; # соединяем бинарные данные, с ASCII данными

print "bin_a length:\t";
print length($bin_a) . "\t" . bytes::length($bin_a) ."\n";

my $bin_u = $bin.$ascii_u; # опять соединяем бинарные данные, с ASCII данными

print "bin_u length:\t";
print length($bin_u) . "\t" . bytes::length($bin_u) ."\n";

print "bin_a and bin_u are equal!\n" if $bin_a eq $bin_u;

open my $f, ">", "file_a.tmp";
binmode $f;
print $f $bin_a;
close $f;

open $f, ">", "file_u.tmp";
binmode $f;
print $f $bin_u;
close $f;

system("md5sum file_?.tmp"); # md5sum - команда linux


выдаёт:

original bin length:    3       3
bin_a length:   4       4
bin_u length:   4       7
bin_a and bin_u are equal!
33818f4b23aa74cddb8eb625845a459a  file_a.tmp
33818f4b23aa74cddb8eb625845a459a  file_u.tmp


В результате получается, что бинарные данные, после конкатенации с 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). Для символов с кодом <= 127 UTF-8 бит не важен, в том плане, что их можно рассматривать и как бинарные данные, и как символы. Для символов Latin1 — отличить от байтов не возможно.

Отличить байты от символов в Perl — это всё равно что отличить имя файла от email и от имени человека. Иногда возможно, но в общем случае — нет. Сам программист должен помнить в какой переменной что у него находится.

Это есть в документации:

perldoc.perl.org/perlunifaq.html

How can I determine if a string is a text string or a binary string?

You can't. Some use the UTF8 flag for this, but that's misuse, and makes well behaved modules like Data::Dumper look bad. The flag is useless for this purpose, because it's off when an 8 bit encoding (by default ISO-8859-1) is used to store the string.

This is something you, the programmer, has to keep track of; sorry. You could consider adopting a kind of «Hungarian notation» to help with this.


Если вам всё же нужно это сделать, можно создать свой класс, который будет содержать строку байтов или символов, и флаг, показывающий что это (тот же трюк подойдёт для email vs имя файла vs имя человека).

Wide characters не выдаётся для символов из Latin-1


Слудеющий пример выдаёт warning Wide characters in print только если мы печатает $s2

use strict;
use warnings;
use utf8;
my $s1 = "ß";
my $s2 = "тест";
my $s = $ARGV[0] ? $s1 : $s2;
print $s;


Если мы печатем $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 все параметры передаются по ссылке, но обычно используются по значению.

sub mycode
{
  $_[0] = "X"; # модифицировали первый фактической параметр, не зависимо от воли вызывающего
}


sub mycode
{
  my ($arg1) = @_; # типичный способ работы с аргументами функции
  $arg1 = "X"; # теперь параметр доступен по значению, фактический параметр не модифицируется
}


Таким образом, при создании кода, который работает точно в соответствие со спецификацией Perl, создаётся код, незаметно делающий utf8::downgrade над фактическими параметрами, не зависимо от воли вызывающего, тем самым, возможно, создав баг в каком-то другом месте, которое неправильно обрабатывало строки, и до этого момента работало хорошо.

Для имён файлов всё это не работает


Функции, которые принимают имена файлов как аргументы (open, файловые тесты -X), а так же, которые возвращают имена файлов (readdir), не подчиняются этим правилам (это отмечено в документации).

Они просто интерпретируют имя файла, как оно есть в памяти.

Алгоритм их работы можно описать следующим образом:

sub open {
 my ( ... $filename) = @_;
 utf8::_utf8_off($filename); # теперь это двоичные данные
 _open($filename);


Для этого есть несколько причин:

1. Во многих POSIX системах ( Linux / *BSD ), на многих файловых системах, именем файла может являться произвольная последовательность байтов, не обязательно являющаяся последовательностью символов в какой-либо кодировке.
2. Нет переносимого способа определить кодировку файловой системы.
3. На машине может быть несколько файловых систем с разной кодировкой
4. Нельзя опираться на предположение, что кодировка имён файлов совпадёт с кодировкой локали.
5. Должна быть совместимость со старым кодом.

В итоге программист должен сам определять кодировку и сообщать её интерпретатору, но API для этого ещё не сделали.

Модифицируем наш пример, где мы «случайно» наткнулись на downgrade строки символов.

use strict;
use warnings;
use Digest::SHA qw/sha1_hex/;
use utf8;
my $s = "µ";
my $s1 = $s;
my $s2 = $s;

my $digest = sha1_hex($s2); # попробуйте закомментировать эту строчку

print STDERR "s1 and s2 are equal\n" if $s1 eq $s2;

open my $f, ">", "$s1.tmp" or die "s1 failed: $!";
print $f "test";
close $f;

open $f, "<", "$s2.tmp" or die "s2 failed: $!";
print STDERR "Done\n";

Результат работы:
s1 and s2 are equal
s2 failed: No such file or directory 


т.е. строки 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 году.

Но майнтайнер отвергает все подобные багрепорты. При этом суть репортов, даже не в том чтобы функция вела себя правильно (в соответствии с идеей Perl), и даже не в том, чтобы к ней была документация, описывающая это поведение, а в том, что, хотя бы, это поведение не должно противоречить существующей документации. Майнтайнер же считает, что функции документированы как эквивалентные, а это не значит идентичные (хотя помоему эквивалентность может рассматриваться и как похожесть, и как идентичность). Возможно в математике эквивалентность не содержит даже намёка на идентичность… Если кто-то сможет разгадать эту загадку, буду очень благодарен.

The Unicode Bug


В downgraded форме Latin-1 нельзя отличить от байтов, следовательно, в этой форме, плохо работают некоторые метасимволы в регулярных выражениях, функции uc, lc, quotemeta.

Воркэраунд — utf8::upgrade, либо, в новых версиях Perl — некоторые директивы, которые позволяют сделать это поведение консистентным.

Подробное описание в документации Perl

Что же делать со всем этим?


1. Не пользуйтесь (если вы точно не знаете, что делаете) следующими функциями: utf8::is_utf8, Encode::_utf8_on, Encode::_utf8_off, и всеми функциями из модуля bytes (документация ко всем этим функциям не рекомендует их использование, кроме как для отладки)

2. Пользуйтесь utf8::upgrade, utf8::downgrade, всякий раз, когда этого требует спецификация Perl

3. Для конвертации из символов в байты пользуйтесь Encode::encode, Encode::decode

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

4. При работе с именами файлов, либо придётся использовать wrapper над всеми функциями, либо, с помощью тестов убедиться, что внутреннее представление имён файлов не меняется в процессе работы кода.

Есть несколько примеров, когда нарушение этих правил мне показалось оправданным.

Encode::_utf8_off($_[0]) if utf8::is_utf8($_[0]) && (bytes::length($_[0]) == length($_[0]));

(сбрасывет UTF-8 флаг для ASCII-7bit текста (тем самым удаётся достичь 30% увеличения производительности регэкспов, во всех Perl, кроме 5.19)

defined($_[0]) && utf8::is_utf8($_[0]) && (bytes::length($_[0]) != length($_[0]))

(Возвращает TRUE, если у строки установлен UTF-8 флаг, и при этом она не является ASCII-7bit. Может использоваться в unit тестах, чтобы убедиться, что ваши 400 мегабайт бинарных данных не превращаются в 700)

Есть ещё вариант ничего не делать. Честно говоря, пройдёт довольно много времени, прежде чем вы наткнётесь на какой-либо баг (но, к тому моменту будет уже поздно). Этот вариант крайне не рекомендуется для разработчиков библиотек.

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 21

    0
    В статье есть некоторые ошибки.

    Функция utf8::is_utf8() к сожалению показывает не наличие флага у строки, а внутреннее это представление или нет.

    Строка «аа» во внутреннем представлении перла без флага выглядит так: \x{d0}\x{b0}\x{d0}\x{b0}
    а с флагом вот так: \x{430}\x{430}

    И функция utf8::is_utf8() на оба варианта вернёт истину.

    В каком виде строка лучше всего смотреть через Data::Dumper.
    Берём простой пример

    use Data::Dumper;
    use Modern::Perl;

    my $a = «аа»;
    my $z = «яя»;

    print Dumper( $a );

    utf8::upgrade( $a ); # перевели во внутреннее представление только чтобы показать что оно отличается от флаговой стоки

    print Dumper( $a );

    utf8::decode( $a );

    print Dumper( $a );

    my $qq = "$z. $a";

    print Dumper( $qq );

    Его результат будет таким:
    $VAR1 = 'аа';
    $VAR1 = "\x{d0}\x{b0}\x{d0}\x{b0}";
    $VAR1 = "\x{430}\x{430}";
    $VAR1 = "\x{d1}\x{8f}\x{d1}\x{8f}. \x{430}\x{430}"

    А то что \x{d1}\x{8f}\x{d1}\x{8f} — это не флаговая строка лучше всего Вам покажет regexp,
    в нефлаговых строках \s пересекается с кириллицей по 1 букве в маленьких и заглавных буквах:

    my $a = «абвгдеёжзийклмнопрстуфхцчшщъыьэюя»;
    utf8::upgrade( $a );
    print «1\n» if $a =~ /\s/;

    напечатает единицу, а код

    my $a = «абвгдеёжзийклмнопрстуфхцчшщъыьэюя»;
    utf8::decode( $a );
    print «1\n» if $a =~ /\s/;

    ничего не напечатает — потому что мы повесили флаг на $a и буква «х» стала единым целым,
    а не двумя половинками, одна из которых совпадает с пробельным символом.

    PS чтобы использовать Encode::encode, Encode::decode не обязательно подключать модуль Encode,
    проще использовать utf8::decode() и utf8::encode() чтобы не засорять namespace модуля/скрипта
      0
      Строка «аа» во внутреннем представлении перла без флага выглядит так: \x{d0}\x{b0}\x{d0}\x{b0}
      а с флагом вот так: \x{430}\x{430}

      И функция utf8::is_utf8() на оба варианта вернёт истину.

      Нет.

      my $s1 = "x{d0}\x{b0}\x{d0}\x{b0}";
      print "s1:", utf8::is_utf8($s1), "\n";
      my $s2 = "\x{430}\x{430}";
      print "s2:", utf8::is_utf8($s2), "\n";
      

      s1:
      s2:1
      


      utf8::upgrade( $a ); # перевели во внутреннее представление только чтобы показать что оно отличается от флаговой стоки


      Нет,

      use Data::Dumper;
      use Modern::Perl;
      
      my $a = "аа";
      
      print length($a);
      
      


      печатает 4.

      Т.к. буква «a» здесь вовсе не буква, а последовательность байтов, которыми эта буква кодируется в UTF-8 (это потому-что нет «use utf8»).

      Соответственно utf8::upgrade над этой строкой, интерпретирует эти байты как символы в кодировке Latin1 и меняет внутреннее представление на Unicode (с utf-8 флагом).

      Дальше этот пример не изучал.

      Функция utf8::is_utf8() к сожалению показывает не наличие флага у строки, а внутреннее это представление или нет.


      Может дело в терминологии? utf8 флаг и показывает внутреннее представление. Если он установлен, то строка — это «массив целых чисел» закодированный в UTF-8. Если флага нет — значит «массив целых чисел», где каждый октет — одно число.

      («массив целых числел» чтобы не путать с символами, ведь байты во «внутреннем представлении» могут быть тоже в UTF-8)

        0
        В документации прямо и пишут «the internal representation», т.е. внутреннее представление, а не flaged или что-то подобно
        Во внутреннем представлении можно писать символы в любой кодировке.
        Вот, например, украинские буквы из cp1251:
        \xAA\xBA\xA5\xB4\xB2\xB3\xAF\xBF — это єіїґ, маленкие и большие,
        и к utf8 они никаким боком не относятся.

        Вы даже сами процитировали одну из строк с этим термином.
        И этот термин был ещё до появления utf8 в перле — поищите документацию по первым версиям 5-го перла.

        > my $s1 = «x{d0}\x{b0}\x{d0}\x{b0}»;

        Это некорректное присвоение, т.е. Вы не то присвоили, это Dumper так выводит стоки.
        Надо было
        my $s1 = 'аа';
        utf8::upgrade($a);

        Соответсвенно и результат не тот.

        Строка в utf8 с флагом — это именно символы, а без флага — байты, это тоже можно проверить regexp-ом
          0
          В документации прямо и пишут «the internal representation»

          utf-8 флаг это и есть единственное различие во внутреннем представлении строк. Пишут «internal representation» т.к. не хотят чтобы люди думали о utf8 флаге (его использование считается опасным, про это и пост)

          Это некорректное присвоение, т.е. Вы не то присвоили, это Dumper так выводит стоки.

          Он именно так выводит строки, чтобы их можно было прочитать обратно в Perl. Формат его вывода — perl.

          Надо было
          my $s1 = 'аа';
          utf8::upgrade($a);

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

          Строка в utf8 с флагом — это именно символы, а без флага — байты, это тоже можно проверить regexp-ом

          нет, весь пост про то, что это не так)

          То что регэксп не работает — это The Unicode Bug (тоже описан в посте, со ссылкой на документацию).

          p.s.

          уже после написания поста, нашёл похожую статью на английском blogs.perl.org/users/aristotle/2011/08/utf8-flag.html

          p.p.s.
          полностью увидеть внутреннее представление скаляра поможет Devel::Peek Dump, там точно видно внутреннее представление строки (правда не уверен, что Data::Dumper где-то хуже)
            0
            > То что регэксп не работает — это The Unicode Bug

            Для regexp-а строки 'аа' и x{d0}\x{b0}\x{d0}\x{b0} выглядят одинакого — он видит байты, не символы.

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

            В mysql, например, из-за этого пришлось вводить разные функуции: длинна в байтах leght(), в символах — char_leght()
              0
              > нашёл похожую статью на английском

              Просто ещё одна неверная интерпретация из-за функции utf8::is_utf8()
              Именно она и utf8 valid вносят кучу неразберихи и непонимания как оно работает внутри.
              В результате люди в коде делают баги — нередко видел что после utf8::is_utf8() думают что в строке utf8-символы,
              а это не всегда так (да и сам лет 5 назад такие ошибки допускал).
                0
                Это один из авторов Perl написал.
                  0
                  Ну значит он один из тех кто создаёт терминологическую путаницу.

                  Везде говоря что строка с флагом по умолчанию понимают что мы работаем с символами, а без флага — с байтами.
                    0
                    строка с флагом по умолчанию понимают что мы работаем с символами, а без флага — с байтами.

                    Это заблуждение я и хотел развеять своим постом. Где говорят? Пруф?
                      0
                      Просто пообщайтесь с людьми.

                      Не редко программисты даже не знают что могут получить строку, на которую среагирует is_utf8(), но там будут байты.

                      Я летом у на работе нашёл (и потом выкинул) код, который имел автоопределение utf8 по такому алгоритму:
                      получили строку $a
                      eval{ decode($a) }
                      проверка на is_utf8, если прошло — значит считаем что теперь у нас utf8 символы

                      А люди, которые использовали его код не могли понять почему он иногда глючит.
            0
            > Дальше этот пример не изучал.

            Я изучал — с байтами upgrade/downgrade работают, переводя строки между внетренним и обычным представлением строки, а вот символы бывают только во внутреннем, поэтому upgrade/downgrade для них бесполезны.

            Ещё более интересны encode/decode.

            utf8::decode может поднять строку только до символов и останавливается, а вот utf8::encode может бесконечно менять строку

            код:
            my $a = «ая»;

            print Dumper( $a );
            utf8::decode( $a );
            print Dumper( $a );
            utf8::decode( $a );
            print Dumper( $a );
            utf8::encode( $a );
            print Dumper( $a );
            utf8::encode( $a );
            print Dumper( $a );
            utf8::encode( $a );
            print Dumper( $a );
            utf8::encode( $a );
            print Dumper( $a );

            результат:
            $VAR1 = 'ая';
            $VAR1 = "\x{430}\x{44f}";
            $VAR1 = "\x{430}\x{44f}";
            $VAR1 = 'ая';
            $VAR1 = 'аÑ';
            $VAR1 = 'ðÃÂ';
            $VAR1 = 'ÃÂðÃÂÃÂ';

            decode() строки вида 'аÑ'; и др. понимает и приводит из к нормальному виду, что бывает полезно.
              0
              > Дальше этот пример не изучал.
              Я изучал

              Если вы запостите этот пример авторам perl (чтобы проиллюстрировать что-нибудь, в багрепорте ит.д.), никто его не будет изучать дальше этих строчек, пока Вы не объясните зачем конвертировать UTF-8 текст из Latin1 в UTF-8… Если эта операция лишена смысла, то этот пример кода смотреть не будут, т.к. там баг.
            0
            PS чтобы использовать Encode::encode, Encode::decode не обязательно подключать модуль Encode,
            проще использовать utf8::decode() и utf8::encode() чтобы не засорять namespace модуля/скрипта

            utf8::decode не обрабатывает ошибки, что чревато падением скрипта в другом месте, испорченными данными, и, может, security проблемами.

            В документации есть строчки
            Note that this function does not handle arbitrary encodings. Therefore Encode is recommended for the general purposes; see also Encode.
              0
              Да, действительно, Data::Dumper выводит строки в формате perl.
              И для некоторых строк это означает потерю UTF-8 флага (совершенно законную).
              см. perldoc

              Так что давайте заново.

              Фраза

              Строка «аа» во внутреннем представлении перла без флага выглядит так: \x{d0}\x{b0}\x{d0}\x{b0}


              1. не имеет смысла, если Вы приводите вывод Data::Dumper
              2. не правда, если толковать её буквально: $s="\x{d0}\x{b0}\x{d0}\x{b0} "

              И функция utf8::is_utf8() на оба варианта вернёт истину.

              конечно, потому что у обоих установлен utf-8 флаг. Собственно определение UTF-8 флага — это то что возвращает эта функция.
              Так же используйте Devel::Peek вместо Data::Dumper

              То, что Вам Data::Dumper выводит на экран, уже может быть лишино информации о UTF-8 флаге.

              p.s. и я уже говорил, про то что Ваш пример делает что-то не то — конвертирует utf8 из latin1 в utf8.
              0
              В Encode.pm описание is_utf8 — "...Tests whether the UTF8 flag is turned on in the string. If check is true also checks whether string contains well-formed UTF-8", тогда как utf8::is_utf8 (в utf8.pm) — «Test whether string is in UTF-8 internally. Functionally the same as Encode::is_utf8().»

              Если посмотреть исходный код. Encode::is_utf8 (Encode.xs)– использует также bool Perl_is_utf8_string(const U8 *s, STRLEN len).

              А utf8::is_utf8 — это в universal.c
                  {"utf8::is_utf8", XS_utf8_is_utf8, NULL},
              XS(XS_utf8_is_utf8)
              {
                   dVAR;
                   dXSARGS;
                   if (items != 1)
              	 croak_xs_usage(cv, "sv");
                   else {
              	SV * const sv = ST(0);
              	SvGETMAGIC(sv);
              	    if (SvUTF8(sv))
              		XSRETURN_YES;
              	    else
              		XSRETURN_NO;
                   }
                   XSRETURN_EMPTY;
              }
              

              Если сравнить то и там и там используется SvUTF8, но в модуле Encode дополнительно выполняется проверка
                  if (RETVAL &&
                      check  &&
                      !is_utf8_string((U8*)SvPVX(sv), SvCUR(sv)))
                      RETVAL = FALSE;
              

              Само определение SvUTF8 есть в sv.h
              #define SvUTF8(sv)		(SvFLAGS(sv) & SVf_UTF8)
              #define SVf_UTF8        0x20000000  /* SvPV is UTF-8 encoded
              #define SvFLAGS(sv)	(sv)->sv_flags
              

              Типы в Perl определяются структурами типа SV (см. sv.h), которые имеют поле sv_flags. Как видим, макрос SvUTF8 выполняет проверку установлен ли флаг is UTF-8 encoded и больше ничего.

              Таким образом, utf8::is_utf8 просто проверяет установлен ли этот бит (флаг), который конечно же можно устанавливать с помощью SvUTF8_on (ниже побитовый or, который это делает в макросе)
              #define SvUTF8_on(sv)		(SvFLAGS(sv) |= (SVf_UTF8))
              

              а вот Encode::is_utf8 еще и выполняет is_utf8_string (файл utf8.c) о чем было замечено vadiml, которая «…Returns true if the first bytes of string form a valid UTF-8 string, false otherwise….» Для проверки функция старается использовать макрос UTF8_IS_INVARIANT, который определен в utf8.h, иначе более медленный вариант в ветке else.

              Я думаю, вывод такой, utf8::is_utf8 — работает очень быстро, но ее не следует использовать на данных получаемых извне, а Encode::is_utf8 – более медленный вариант, но зато выполняет проверку.
                0
                В Encode.pm описание is_utf8 — "...Tests whether the UTF8 flag is turned on in the string. If check is true also checks


                да, интересно, не замечал этого различия.
                Perl_is_utf8_string действительно проверяет является ли строка валидным UTF-8.

                Думаю Encode::is_utf8 с флагом check эквивалентно utf8::valid().

                Я лично вижу тут различные функции. utf8::is_utf8() — проверит UTF-8 флаг у строки. utf8::valid() — проверить
                является ли строка валидной UTF-8.

                Опять же, наличие или отсутствие UTF-8 флага или даже валидности UTF-8 ничего не говорит о данных (бинарные vs текстовые). Произвольная JPEG картинка в Perl
                может быть с UTF-8 флагом, и быть валидным UTF-8. При этом при любых действиях с ней, конвертироваться обратно в байты (можно посчитать md5 от неё, и он будет md5 от байтов, а не от UTF-8 «представления»)

                Я бы рассматривал UTF-8 строку как «массив целых чисел». Т.е. Perl может использовать UTF-8 формат для хранения случайных «чисел», без оглядки на стандарт Unicode.

                Кстати, по этой причине, utf8::valid() не надёжен для валидации текста в UTF-8. Он может вернуть истину для данных, которых нет в стандарте Unicode.

                пруф:
                perldoc.perl.org/perlunifaq.html#What%27s-the-difference-between-UTF-8-and-utf8?
                rt.perl.org/Public/Bug/Display.html?id=43294#txn-1256493

                Лучше использовать Encode::decode(). Или (здесь пока не уверен), самая надёжная штука, это модуль search.cpan.org/perldoc?Unicode%3A%3AUTF8
                +1
                Думаю Encode::is_utf8 с флагом check эквивалентно utf8::valid().
                — получается не так. В Encode::is_utf8 с флагом check выполняется
                    RETVAL = SvUTF8(sv) ? TRUE : FALSE;
                    if (RETVAL &&
                        check  &&
                        !is_utf8_string((U8*)SvPVX(sv), SvCUR(sv)))
                        RETVAL = FALSE;
                

                Получается, что TRUE если есть установленный флаг и не выполняется условие ниже. Условие ниже не выполняется также когда check=0, в этом случае также не будет выполнятся проверка !is_utf8_string. То есть, когда check=0 – то это полный аналог utf8::is_utf8 (также если флаг не установлен). Только в случае если check=1 и установлен флаг Encode::is_utf8 выполняет проверку is_utf8_string. То есть, возвращаем true когда есть флаг + проходим проверку.

                По utf8::valid – если флаг установленный выполняем проверку. Если она проходит – значит ОК. Если флаг не установлен – возвращаем ОК без проверки. Название вводит в заблуждение, наверное, хотя так и должно быть – “… [INTERNAL] Test whether STRING is in a consistent state regarding UTF-8. Will return true if it is well-formed UTF-8 and has the UTF-8 flag on or if STRING is held as bytes (both these states are 'consistent'). Main reason for this routine is to allow Perl's testsuite to check that operations have left strings in a consistent state. You most probably want to use utf8::is_utf8() instead.”

                    {"utf8::valid", XS_utf8_valid, NULL},
                
                XS(XS_utf8_valid)
                {
                     dVAR;
                     dXSARGS;
                     if (items != 1)
                	 croak_xs_usage(cv, "sv");
                    else {
                	SV * const sv = ST(0);
                	STRLEN len;
                	const char * const s = SvPV_const(sv,len);
                	if (!SvUTF8(sv) || is_utf8_string((const U8*)s,len))
                	    XSRETURN_YES;
                	else
                	    XSRETURN_NO;
                    }
                     XSRETURN_EMPTY;
                }
                

                  0
                  Получается, что TRUE если есть установленный флаг и не выполняется условие ниже

                  Да, действительно. И даже в документации так же написано.

                  Если флаг не установлен – возвращаем ОК без проверки.


                  А мне кажется это более логичным. Если флаг не установлен, значит все «символы» в строке — от 0 до 255.

                  Will return true if it is well-formed UTF-8 and has the UTF-8 flag on or if STRING is held as bytes (both these states are 'consistent').


                  Так как внутреннее представление строки не важно при всех операциях,
                  и строка байтов может быть проагрейжена до UTF-8 строки (и будет ей равна — «eq»), то строка байтов всегда является валидной строкой.

                  Собственно эта функция всё равно не пригодна для валидации Unicode UTF-8 символов, и единственное её назначение — проверка внутреннего состояния строк Perl.

                  Encode::is_utf8 же вроде как создана для валидации текста, с другой стороны не пригодна для этого (см. мой предыдущий коммент выше), так что её поведенее кажется мне более странным.
                  +1
                  Т.е. перекодировка туда-обратно не меняет данные (это ожидаемо), но устанавливает UTF-8 флаг.
                  (впрочем, такое поведение decode() противоречит его собственной документации, которая, в свою очередь, противоречит идее, что никакой документации и гарантий относительно utf-8 флага в ASCII данных быть не должно)

                  Исходя из реализации в Encode.xs — Method_decode_xs там явно выполняется в конце SvUTF8_on(dst); Я не нашел «явного» исключения для ASCII,EBCDIC. Все мои попытки в Perl коде заставить не выставлять флаг utf8 для decode, также ни к чему не привели.

                  Судя по-всему это неточность в документации, что уже обсуждалось здесь — rt.cpan.org/Public/Bug/Display.html?id=34259, в том числе и автором поста :).
                    +1
                    Я думаю, многие были введены в заблуждение формулировкой, что внутренним форматом (internal format) в Perl является utf-8. Эта фраза встречается и в документации к модулям, например в описании Encode::decode — "..this function returns the string that results from decoding the scalar value OCTETS, assumed to be a sequence of octets in ENCODING, into Perl's internal form", который конвертирует в utf8 + выставляет флаг utf8.

                    Есть хороший ответ на данный вопрос — www.perlmonks.org/?node_id=551676#i_lost_track__what_encoding_is_the_internal_format_really — "… by default, the internal format is either ISO-8859-1 (latin-1), or utf8, depending on the history of the string.."

                    В коде
                    use strict;
                    use warnings;
                    use Devel::Peek;
                    
                    my $str = "Hello";
                    print Dump ($str);
                    

                    для $str используется в качестве внутреннего формата Latin-1. Если мы добавим в этот же код use utf8; все равно будет использоваться Latin-1.

                    Этот код, написанный в utf-8 опять же использует Latin-1. Мы не дали подсказки с помощью use utf8; для интерпретатора, что же такое $str. Когда мы укажем use utf8 — будет использоваться UTF-8.
                    use strict;
                    use warnings;
                    #use utf8;
                    use Devel::Peek;
                    
                    my $str = "Привет";
                    print Dump ($str);
                    


                    Этот код, набранный в cp1251, будет использовать UTF-8 в качестве внутреннего формата
                    use strict;
                    use warnings;
                    use encoding('windows-1251');
                    use Devel::Peek;
                    
                    my $str = "Привет";
                    print Dump ($str);
                    


                    А это код (который набран в utf-8), казалось бы должен использовать utf8 в качестве внутреннего формата, но это не так — используется Latin-1, наверное из соображений эффективности :).
                    use strict;
                    use warnings;
                    use utf8;
                    use Devel::Peek;
                    
                    my $str = "Hello";
                    print Dump ($str);
                    


                      0
                      Всё правильно, только я вместо «внутреннего формата Latin-1» представляю себе что внутренний формат «байты», который интерпретируется как Latin1 в строковом контексте.

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое