Perl の URI モジュールにおける文字列とオクテット列の扱いに関して2011年05月27日 17時28分

Perl の URI モジュールには ASCII 外の文字の扱いに関して欠陥がある。ここでは Perl における URI の扱いについて述べ、URI モジュールの修正方針を提案する。

この記事で取り上げた問題に関して主たる部分は URI.pm 1.59 で (この提案とは違った形で) 修正済みであり、この提案は obsolete です。

用語の定義

URI
RFC 3986 (日本語訳) で定義される識別子。
URI.pm
Perl の URI モジュール。ここでは、ファイルとしての URI.pm だけでなく、URI パッケージ下に含まれるコード全般を指す。断りがない限りバージョン 1.58 に基づく。
文字列
UTF-8 文字列と Latin-1 文字列のいずれか。
UTF-8 文字列
Perl の文字列値で、UTF8 フラグが立っているもの。
Latin-1 文字列
Perl の文字列値で、UTF8 フラグが立っておらず、文字の並びとして扱われることを期待されているもの。
オクテット列
Perl の文字列値で、UTF8 フラグが立っておらず、バイトの並びとして扱われることを期待されているもの。Latin-1 文字列とオクテット列の機械的な判別は不可能。

発端となった問題

URI.pm のメソッドに UTF-8 文字列とオクテット列を同時に渡した場合、期待した出力が得られないことがある。特に Perl 5.10 以下では、use utf8 環境下で bareword に UTF8 フラグが立つため、この問題が発生しやすい。

#!/usr/bin/perl5.8.8
use strict;
use warnings;
use utf8;
use URI;
use Encode qw/encode_utf8/;

my $uri = URI->new('http://example.org/');
$uri->query_form( foo => encode_utf8('字') );
print $uri->query, "\n";
# actual:   "foo=%C3%A5%C2%AD%C2%97"
# expected: "foo=%E5%AD%97"

Perl において URI はどう扱われるべきか

URI は US-ASCII の範囲内の文字で構成されるが、特定の文字符号化方式を仮定しない。URI には任意の文字符号化方式で符号化されたオクテットの並びが (パーセントエンコードされて) 含まれうる。たとえば、二つの URI http://ja.wikipedia.org/wiki/%E3%81%AF%E3%81%A6%E3%81%AAhttp://d.hatena.ne.jp/keyword/%A4%CF%A4%C6%A4%CA において、パーセントエンコードされた部分はいずれも「はてな」という文字の並びを表すが、前者は UTF-8、後者は EUC-JP で符号化され、パーセントエンコードされたものである。

それゆえ、Perl において URI はオクテット列として扱われるべきである。そのオクテット列がどの文字符号化方式を用いて解釈されるかは、URI とは別の層の問題である。

URI.pm をどう修正すべきか

現在、URI.pm では入力を文字列として扱うものとオクテット列として扱うものが混在している。たとえば、host メソッドの引数に渡した値は UTF-8 文字列または Latin-1 文字列として解釈され、query メソッドの引数に渡した値はオクテット列であるかのように扱われる。

ここでは、前節の主張に従い、URI.pm に関する入出力はすべてオクテット列として扱うことを提案する。IRI に関するものを除き、URI.pm の各メソッドに渡された引数はすべてオクテット列とみなされ、返り値はオクテット列となるべきである。

引数に UTF-8 文字列が渡された場合は、それを UTF-8 で符号化されたオクテット列として扱うのがよいと考える。HTTP::Messagecontent メソッドのように UTF-8 文字列を渡されたらエラーとするのもひとつの手ではあるが、現在の URI.pm との互換性を大きく損ねないためにはエラーにしないほうがよいと判断する。

国際化ドメイン名の扱い

URI.pm の入出力をオクテット列とみなすとき、問題となるのは国際化ドメイン名の扱いである。本来なら ASCII 外の文字を含めるのに URI::IRI のような専用のモジュールを使うべきだろうが、現在の URI.pm との互換性を損ねないためには、URI->new(...)$uri->host(...) で ASCII 外の文字を指定できるのが好ましい。

あくまで入力はオクテット列とみなすという方針を貫きつつ国際化ドメイン名を受け付けるため、URI のホスト部に関してはオクテット列を UTF-8 で符号化されたものとみなし、UTF-8 でデコードした文字列を Punycode 符号化して出力のホスト部とする。

URI.pm の互換性の確保

ここで提案する修正により、URI.pm は後方互換性を失う。特にホスト部に関しては、これまで Latin-1 文字列として扱われてきたものが、UTF-8 で符号化されたオクテット列として扱われ、まったく違った出力を得ることになる。

そこで、過去の挙動に戻せるよう、$URI::COERCE_OCTETS 変数を設ける。この変数のデフォルト値は真であるが、値が偽なら以前と同様に入力をオクテット列とみなさず処理を行う。

修正の段取り

以上の提案を実装すべく、現在 github の nanto/uri レポジトリの coerce_octets ブランチで作業中である。さしあたってどのような出力を期待しているのか明らかにするため、ASCII 外の文字を含む入出力に関するテストを書いた。実装が一段落したら URI.pm に対するパッチとして提出するつもりである。

#43859: should be _utf8_off -ed raw data before URI encoding修正の URL を添えて返信した。

コメント

_ nanto_vi ― 2011年05月28日 21時09分

https://twitter.com/miyagawa/status/74166796504141824
https://twitter.com/miyagawa/status/74167063391903744

The bareword example is just a case of problems caused by the way URI.pm treats strings. For another example, think about a URL with IDN, http://日本語.jp/?q=%E5%AD%97 .

var $uri = URI->new("http://\x{65E5}\x{672C}\x{8A9E}.jp/?q=%E5AD%97");
$uri->host; # "xn--wgv71a119e.jp"
$uri->query_param('q'); # "\x{e5}\x{ad}\x{97}" (UTF8 flag is on)

$uri->query_param('q') returns a UTF8-flagged string "\x{e5}\x{ad}\x{97}". I think it should return octets "\xe5\xad\x97". query_param method returns octets if we pass octets to URI->new, but then a host become different from the string case.

var $uri = URI->new(encode_utf8("http://\x{65E5}\x{672C}\x{8A9E}.jp/?q=%E5AD%97"));
$uri->host; # "xn--xakg0a0al61bcu.jp"
$uri->query_param('q'); # "\xe5\xad\x97" (UTF8 flag is off)

I think in both case host should return "xn--wgv71a119e.jp" and query_param should return octets "\xe5\xad\x97".

By the way, I used utf8::encode just to make UTF8 flag off. I now think what I wanted is Encode::_utf8_off.


baweword が絡んだ問題は URI.pm の挙動に疑問を抱いたきっかけに過ぎない。私が URI.pm 自体に手を入れる必要があると感じたのは、国際化ドメイン名を使いつつ URI の各部をオクテット列に保つスマートな方法がわからなかったからである。

今 URI のバグリストを見直していて https://rt.cpan.org/Public/Bug/Display.html?id=43859 の存在に気づいた。記事中でこちらも参照すべきだった。

_ のむヨーグルト(ブルーベリー) ― 2012年01月05日 12時50分

オクテット列だけのサポートでよいと思う。UTF-8と互換性を持たせるのは混乱のもと。

_ nanto_vi ― 2012年01月08日 16時14分

私の提案したUTF-8云々は過去のURI.pmと互換性を持たせるための処置でしたが (といってもhostメソッドの動作が変わる時点で中途半端な互換性だったのですが)、URI.pm 1.59ではより過去互換性を維持した形でURIをオクテット列として扱うような変更が加えられました。

コメントをどうぞ

※メールアドレスとURLの入力は必須ではありません。 入力されたメールアドレスは記事に反映されず、ブログの管理者のみが参照できます。

※投稿には管理者が設定した質問に答える必要があります。

名前:
メールアドレス:
URL:
次の質問に答えてください:
「ハイパーテキストマークアップ言語」をアルファベット4文字でいうと?

コメント:

トラックバック

このエントリのトラックバックURL: http://nanto.asablo.jp/blog/2011/05/27/5883636/tb