とあるモバイル系エンジニアの日々

2009年02月08日

URIモジュールにutf8フラグつきの文字列を食わせたらquery_formのエンコードが化けたでござるの巻

タイトル長い。っていうかそのまんまです。


#!/usr/bin/perl

use strict;
use warnings;

use URI;

my $s = 'http://example.com/?q=%82%e2%82%e9%95v%82%c5%8aw%82%d4';
utf8::upgrade($s);

my $uri = URI->new($s);

my %qf = $uri->query_form;
$qf{flag} = 'ON';

$uri->query_form( %qf );

my $uri_str = $uri->as_string;

これ、一見すると$uri_strは

http://example.com/?q=%82%e2%82%e9%95v%82%c5%8aw%82%d4&flag=ON

になりそうなんですが、実際には

http://example.com/?q=%C2%82%C3%A2%C2%82%C3%A9%C2%95v%C2%82%C3%85%C2%8Aw%C2%82%C3%94&flag=ON

になります*1

ここで、qの値である「%82%e2%82%e9%95v%82%c5%8aw%82%d4」は、ShiftJISの文字列です*2


そもそも、何でわざわざ

utf8::upgrade($s);

してんの?とお思いでしょうが、ある局面を擬似的に再現するためセットしたものです。どういう局面かというと、

この場合、取得される値は自動的にutf8フラグが立っています。


つまり、XML::LibXMLを通じてDOM解析して取得してきたURLを、URIモジュールを使ってquery_formメソッドでパラメータ追加したら、予期しないURLになってしまったので、それを調査したときのメモです。(ここまで前フリ)


my %qf = $uri->query_form;

この%qfをDumperしてみると、以下のようになっていました。

### %qf: {
###        q => "\x{82}\x{e2}\x{82}\x{e9}\x{95}v\x{82}\x{c5}\x{8a}w\x{82}\x{d4}"
###      }

qのパラメータの値はなんと、

という目を疑うような状況になっていました。


では、この値を強制的に utf8::downgrade して純粋なShiftJISバイナリにしてやろうじゃないか。

utf8::downgrade($qf{q});

$qf{flag} = 'ON';

$uri->query_form( %qf );

これでもダメ。

### $uri_str: 'http://example.com/?q=%C2%82%C3%A2%C2%82%C3%A9%C2%95v%C2%82%C3%85%C2%8Aw%C2%82%C3%94&flag=ON'

ふと気づいたんですが、

utf8::downgrade($qf{q});

$uri->query_form( q => $qf{q}, flag => 'ON' );

これだとうまくいく。

### $uri_str: 'http://example.com/?q=%82%E2%82%E9%95v%82%C5%8Aw%82%D4&flag=ON'

で、かなり深堀りしていって分かったんだけど、URI::_query という内部モジュールで、

してるんですね。

_query.pm :

sub query
{
    my $self = shift;
    $$self =~ m,^([^?\#]*)(?:\?([^\#]*))?(.*)$,s or die;

    if (@_) {
        my $q = shift;
        $$self = $1;
        if (defined $q) {
            $q =~ s/([^$URI::uric])/ URI::Escape::escape_char($1)/ego;
            $$self .= "?$q";
        }
        $$self .= $3;
    }
    $2;
}

sub query_form {
    my $self = shift;
    my $old = $self->query;
    if (@_) {

# 略...
        my @query;
        while (my($key,$vals) = splice(@_, 0, 2)) {
            $key = '' unless defined $key;
            $key =~ s/([;\/?:@&=+,\$\[\]%])/ URI::Escape::escape_char($1)/eg;
            $key =~ s/ /+/g;
            $vals = [ref($vals) eq "ARRAY" ? @$vals : $vals];
            for my $val (@$vals) {
                $val = '' unless defined $val;
                $val =~ s/([;\/?:@&=+,\$\[\]%])/ URI::Escape::escape_char($1)/eg;
                $val =~ s/ /+/g;
                push(@query, "$key=$val");
            }
        }
        if (@query) {
            unless ($delim) {
                $delim = $1 if $old && $old =~ /([&;])/;
                $delim ||= $URI::DEFAULT_QUERY_FORM_DELIMITER || "&";
            }
            $self->query(join($delim, @query));
        }
        else {
            $self->query(undef);
        }
    }
    return if !defined($old) || !length($old) || !defined(wantarray);
    return unless $old =~ /=/; # not a form
    map { s/\+/ /g; uri_unescape($_) }
         map { /=/ ? split(/=/, $_, 2) : ($_ => '')} split(/[&;]/, $old);
}

つまり何がいいたいかというと、文字列連結してからエンコードしてるんで、キーにutf8フラグつきの文字列が含まれているだけで、連結後の文字列はutf8フラグ立っちゃうということです。


結局、XML::LibXMLから取得した時点で、utf8::downgradeしてやれば問題なし。

my $s = $xml->findvalue('.');
utf8::downgrade($s);

・・気づけば簡単なんだけどね。


ちなみに、

$qf{flag} = 'ON';

してるところがありますが、use utf8; してると、これがあるだけで同じ症状に陥りますので、use utf8;してるなら

{
    no utf8;
    $qf{flag} = 'ON';
}

としてやらんとダメですね。


うーんperlunicode周りはやっぱりハマりやすいなあ・・・・

これ考えると、「基本的に use utf8; してやって、内部では基本的にutf8フラグつき文字列として扱う」というルールに単純化するのも考えものなのかもしれないですねー。utf8のpodにも基本的に必要ないのにuse utf8;すんな、って書いてあるようだし。

追記

ブクマコメントより。

id:nihen氏:

utf8::downgradeはこの場合はASCIIであることが保障されているんであれば問題ないんだけど、意味的にはEncode::encode('latin-1', $s)と同じだからutf8::encodeのほうがよいとおもうがいかがか。

本文中に%qfにShiftJISの文字列がutf8フラグつきで入っていたところがあるところでごっちゃになってしまっていました。(ここではutf8::encodeすると化けてしまうのでutf8::downgradeでないといけない)意味的には確かに utf8::encode の方が妥当ですね。ご指摘ありがとうございます。

id:tokuhirom氏:

必要ないのにuse utf8すんな、という記述はみあたらないのだけど。

utf8のpodから、

PerlscriptUTF-8 で書かれているということを教える目的以外でこのプラグマをつかってはいけません。

実際、本当に UTF-8ソースコードを書こうとするのでないなら、 use utf8 すべきではありません。

のあたりを読んで、「latin-1以外の文字列UNICODEとして扱う意図がなければ、use utf8するな」と解釈したのですが、何か勘違いしてしまっていますかね?

*1Perl 5.8.8 + URI-1.37

*2:『やる夫で学ぶ』

nihennihen 2009/02/11 02:58 >「latin-1以外の文字列をUNICODEとして扱う意図がなければ、use utf8するな」と解釈したのですが、何か勘違いしてしまっていますかね?
うーん。
「必要ないのに」と「目的以外で」ってのは似て非なるものと自分も思ったんですが、日本語としてはかなり微妙ですよね。
なにはともあれソースコードのエンコーディングがUTF-8であればuse utf8するのが現在のPerl Best Practiceではないでしょうか。
むしろそれ以外に選んでよいエンコーディングは"ASCII"か"latin-1"だけという考えなんじゃないかなーとか。それはちょっと踏み込みすぎかな…。
ただバイト列として扱いたいのにuse utf8すべきではないというのは真だとはおもいますってもう1周してどうでもいいなw

nihennihen 2009/02/11 03:03 http://search.cpan.org/~tty/kurila-1.14_0/lib/utf8.pm
一応原文にあたると該当部分は
>Do not use this pragma for anything else than telling Perl that your script is written in UTF-8. The utility functions described below are directly usable without use utf8;.
ってあってutf8::*なメソッドはuse utf8しなくても使えるからUTF-8以外のエンコーディングで書かれてるときにはuse utf8するなよと読めるですよね。

orangevtrorangevtr 2009/02/11 03:14 > id:nihen

早速のコメントどうもありがとうございます。

> なにはともあれソースコードのエンコーディングがUTF-8であればuse utf8するのが現在のPerl Best Practiceではないでしょうか。

そーですね、僕もこのルールでやってました。で、今回のURIトラップにハマったというところです。

ブクマコメントでも書いていましたがURIモジュールはハマりやすいんですね。

> ただバイト列として扱いたいのにuse utf8すべきではないというのは真だとはおもいますってもう1周してどうでもいいなw

そう、実はこういう例外を考慮すると後輩とかに指示するとき苦労するんですよね。最初はこの辺の細かい点は隠蔽したいから「とにかくuse utf8 せえよ」って教えるんですが、今回みたいのが起きると結局深いところまで知っておかないと解決できないですしねえ。。

nihennihen 2009/02/11 03:25 >そう、実はこういう例外を考慮すると後輩とかに指示するとき苦労するんですよね。最初はこの辺の細かい点は隠蔽したいから「とにかくuse utf8 せえよ」って教えるんですが、今回みたいのが起きると結局深いところまで知っておかないと解決できないですしねえ。。

深いところといえるのかどうかわからないんですが、「バイト列」なのか「文字列」なのかというのを常に意識すれば"深いこと"(たとえばUTF8 flagのこと)は知らなくてもいいようには作られてるはずと思いますです。
ただ、CPANモジュールのメソッドの引数が「文字列」を受け取るのか「バイト列」を受け取るのかをドキュメント上はっきりしてないことが多いことは問題な気がしますねー。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証