しばらく前に暴れん坊本屋さんを読んでから、やってみようと考えていたことがある。
というのは、Dual文庫を買ったときにカバーをつけてもらおうということである。
で、ついにその機会が訪れてつけてもらった (というか何もいわなかったらつけてくれた) のだが、さすが書泉というべきか、Dual文庫のサイズに調整されたカバーが用意されていて、なんの問題もなくつけてくれた。
そのカバーはもともと新書用らしく、新書のサイズのところに折り目がついていて、それを詰めて Dual文庫のサイズに調整してあった。
Debian で以下のエラーに出会った。
% /usr/bin/ruby -rnet/https -e ' http = Net::HTTP.new("www.codeblog.org", 443) http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_NONE http.start { req = Net::HTTP::Get.new("/blog/akr/") http.request(req) {|response| p response } } ' /usr/lib/ruby/1.8/net/http.rb:2019:in `read_status_line': wrong status line: "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">" (Net::HTTPBadResponse) from /usr/lib/ruby/1.8/net/http.rb:2006:in `read_new' from /usr/lib/ruby/1.8/net/http.rb:1047:in `request' from -e:7 from /usr/lib/ruby/1.8/net/http.rb:543:in `start' from -e:5
調べてみると、Debian package の libopenssl-ruby1.8 と libruby1.8 のバージョンがずれたことが問題らしい。
% dpkg -l|grep ruby ... ii libopenssl-ruby1.8 1.8.4-1 OpenSSL interface for Ruby 1.8 ... ii libruby1.8 1.8.5-4 Libraries necessary to run Ruby 1.8 ... ii ruby1.8 1.8.5-4 Interpreter of object-oriented scripting lan ii ruby1.8-dev 1.8.5-4 Header files for compiling extension modules
1.8.4 -> 1.8.5 において、use_ssl? が (libruby1.8 の) net/http.rb から (libopenssl-ruby1.8 の) net/https.rb へ移動しており、その libopenssl-ruby1.8 のバージョンがなぜかうまくあがらなかったせいでそのへんの一貫性がおかしくなったのが理由のようだ。
libopenssl-ruby1.8 の状態を見てみると、libruby1.8 (>= 1.8.4) であるから、libruby1.8 の 1.8.5-4 でも動く、といっているわけだが、これは嘘である。
% dpkg -s libopenssl-ruby1.8 Package: libopenssl-ruby1.8 Status: install ok installed Priority: optional Section: interpreters Installed-Size: 584 Maintainer: akira yamada <akira@debian.org> Architecture: amd64 Source: ruby1.8 Version: 1.8.4-1 Replaces: libruby1.8 (= 1.8.3-2) Depends: libc6 (>= 2.3.5-1), libruby1.8 (>= 1.8.4), libssl0.9.8 (>= 0.9.8a-1) Description: OpenSSL interface for Ruby 1.8 Ruby/OpenSSL makes Ruby to be able to use OpenSSL. It includes HTTP and TELNET protocols' SSL/TLS support. . This package provides Ruby/OpenSSL for Ruby 1.8.
さて、この問題を報告する価値はあるだろうか?
まぁ、気にしないで報告することにした。 <URL:http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=410018>
% google-count タミフル タフミル 619000 タミフル 36000 タフミル
URI Template という話があるようだ。
これは
http://example.org/{a}/{b}/
という URI のテンプレートと
a = fred b = barney
という束縛があったときに、
http://example.org/fred/barney/
という URI を生成するというものである。
しかし、internet-draft を眺めると、あからさまにエスケープ回りがおかしい。文章と例が矛盾している。文章では reserved な文字は %-encoding すべしとなっているのに、例ではそうしていないところがある。
なんだこれは、と思って議論されている ML を眺めると、やっぱり指摘されている。
ただ、矛盾があって指摘されたからといって直せるかというとそうとは限らないわけで、そこが難しい。
難しい理由は、URI のエスケープ規則は、それを処理するプログラムに任されているところがあり、URI Template という規格のレベルではその規則を参照できない、というところにある。規則がわからないのに正しいエスケープを定義できるわけがない。
したがって、その規則を外部から与える機構を作るのが正攻法であるが、そうはしないで、個々の reserved の文字毎にエスケープするかどうかを適当に (あるいは熟慮して) 決めるという案が出ている。
そういう案はだいたい見ただけで違和感を覚えるものになるわけで、それにも異論が出ている。
さりとて、規則を個々に指定するというのは記法を決めないといけない。
そんな状況では、いきおい、個々の規則を知らなくても可能で、また、どんな URI でも生成することができる、「そのまま展開しろ」という意見が出てくることになる。
しかしそれはあからさまにエスケープ洩れを誘発するわけでよろしくない。
さて。
エスケープというと、とりあえず端末のエスケープシーケンスと、 C言語における文字列のエスケープシーケンスが思い起こされるが、 Web に関連するエスケープは、後者に近い。
プログラミング言語における文字列のエスケープシーケンスの目的は、以下のものがある。
・ 表現したい文字列をプログラムという文字列の一部に埋め込んで区別する ・ 非表示文字を表示文字として表記する
ここで興味深いのは前者である。
文字列の中に文字列を埋め込むには、埋め込まれる文字列をどこかで始めて、どこかで終らせるという規則が必要になる。
始める方は簡単である。プログラミング言語の文法の中で、ここから始める、という規則を作ればいい。プログラミング言語の文法は、言語設計者が任意に決められるので、そういう規則を作ればよい。 C言語では、ダブルクォーテーション (") があると、そこから文字列が始まる。
問題は、終る方である。埋め込まれる文字列は任意の文字列なので、文字列自身をいくら眺めてもここで終わり、ということはわからない。したがって、文字列の終端を検出するにはなんらかの細工が必要になる。
既存の言語ではさまざまな方法が採用されている。
文字列は有限であるので、その長さが事前にわかっていれば、文字列の終端が検出できる。これを使ったプログラミング言語としては、(昔の) FORTRAN がある。 7HFORTRAN と記述して、長さ7 の FORTRAN という文字列を表現する。
ただ、長さを数値で指定するというのはあまりに非人間的であるせいか、 FORTRAN 以外のプログラミング言語で採用したという例は私は知らない。 (だれか知ってたら教えてください。) ただし、プログラミング言語でなく、人間が扱わないバイナリ形式であれば、長さと中身の組合せで表現するのは珍しくない。
長さをあらかじめ指定しないのであれば、文字列の中身から終端を検出する必要がある。しかし、中身が任意の文字列であれば、中身から終端を検出することはできない。そこで、中身をいくらか制限する必要が出てくる。しかし、任意の文字列を表現するという要求は譲れないので、任意の文字列を制限された文字列で表現するにはどうしたらいいか、という問題が出てくる。
これはヒルベルトのホテルの話に似ている。あの話は各部屋に自然数の名前がついていたが、ここでは文字列の名前がついているわけである。どちらにしても同じ濃度なのでたいした違いはない。
C言語における基本的な解はバックスラッシュ (\) を特別扱いすることである。文字列リテラル内において、バックスラッシュはそのまま記述するのでなく、ふたつ重ねて \\ で表現するのである。そうすると、文字列内においけるバックスラッシュの並びの長さが奇数個であるような文字列は使用せずにすべての文字列を表現することができる。つまり、任意の文字列を制限された文字列で表現するということが実現できた。
ヒルベルトのホテルでいえば、a\b という名前のついた部屋の客を a\\b に移し、 a\\b の部屋の客を a\\\\b に移し、 a\\\b の部屋の客を a\\\\\\b に移し... ということで、結果として、a\b や a\\\b は空室になる。この空室が文字列の終端やエスケープシーケンスを実現するための余裕となる。
エスケープシーケンスはこの空室をそのまま利用する。バックスラッシュが奇数個並んだものを含む文字列は空室であり、意味が割り当てられていないので、新たに意味を割り当てることができる。原理的にはどのような意味を割り当てても良い - 例えばπの小数点以下をバックスラッシュの数だけ並べるとか - が、エスケープシーケンスにおいては、バックスラッシュが 2n+1 個並んでいるとき、バックスラッシュが n 個と、後続の記述に応じたなんらかの機能を表現することにする。たとえば、a\b という表記には、a の後に 2*0+1 個のバックスラッシュが含まれており、後ろに b が続いている。後続の b をバックスペースの意と定義すると、a の後にバックスラッシュが 0 個とバックスペースが並んでいるということになる。 a\\\\\b であれば、a の後にバックスラッシュが 2個とバックスペースである。このように意味を割り当てても、文字列の表現能力は落ちない。
終端はもうひとつ間接的である。
終端はダブルクォーテーションであるが、 abc" という部屋は上記のバックスラッシュ倍増では空かないので、ダブルクォーテーションにバックスラッシュを前置して \" にする、という移動を行う。つまり、abc" という長さ4の文字列は abc\" で表現する。この変換において生じるバックスラッシュは必ず奇数個の連続の終端になる。そのため、文字列全体としてはその部屋はもともと空室であり、文字列の表現能力を落とすことはない。
これで、ダブルクォーテーションが (バックスラッシュが 0個ないし偶数個の連続の後で) 表れるという表記を使わなくても文字列をすべて表現できるようになったので、晴れてその表記を文字列終端に割り当てることができ、文字列の終端を検出できるようになる。
ここで興味深いのは " と \" の変換はバックスラッシュの倍増の後でも可能なことである。たとえば、Ruby でバックスラッシュの倍増は以下のように実装できる。
str = '\arbitrarily"string\containing"double\quotations"and\backslashes"' doubled_backslash = str.gsub(/\\/) { "\\\\" } puts doubled_backslash => \\arbitrarily"string\\containing"double\\quotations"and\\backslashes"
なお、最初の行ではシングルクォートによる文字列を使っているが、このコードの使いかたでは、シングルクォートの間に記述されている文字列がそのまま str に代入される。
そして、倍増したバックスラッシュに影響を与えずに、" を \" に変換することは以下のようにできる。
double_quote_escaped = doubled_backslash.gsub(/([^\\])|\\./) { $1 && $1 == '"' ? '\"' : $& } puts double_quote_escaped => \\arbitrarily\"string\\containing\"double\\quotations\"and\\backslashes\"
また、倍増したバックスラッシュに影響を与えずに、\" に変換されたものを " に戻すことも可能である。
doubled_backslash2 = double_quote_escaped.gsub(/[^\\]|(\\.)/) { $1 && $1 == '\"' ? '"' : $& } puts doubled_backslash2 => \\arbitrarily"string\\containing"double\\quotations"and\\backslashes"
このように " と \" の双方向の変換が可能なことは表現能力の余裕の表れであり、どちらかを終端の表現として割り当てても表現能力が落ちないことの理由でもある。
Ruby の文字列リテラルは、いろいろある。
C言語に似たダブルクォーテーション区切りのもあるが、他にもシングルクォーテーション区切りのもあるし、区切り記号を指定できるのもある。
ダブルクォーテーション区切りの文字列の仕掛けは C言語とほぼ同じである。
シングルクォーテーション区切りはいくらか異なる。
シングルクォーテーション区切りであっても、文字列の終端を検出するために任意の文字列を制限された文字列で表現すること自体は必要であり、バックスラッシュを倍増するのは同じである。
そして、終端の区切りのシングルクォーテーションを区別できるよう、 \' を ' の意味に割り当てるのも (" と ' の違いはあるが) 同様である。
しかし、\' (と \\) 以外のところが違う。ダブルクォーテーション区切りでは、\b がバックスペースになるなど、さまざまな機能を割り当てていたが、シングルクォートでは、\b は \b という記述されたままの文字列に割り当てられる。たとえば、a\b というの表す文字列は、ダブルクォーテーション区切りの文字列では a とバックスペースの 2文字である。しかし、シングルクォーテーション区切りの文字列では a とバックスラッシュと b の 3文字である。
どちらにしてもバックスラッシュ倍増によって空いたところに対する意味付けであり、その部分は使わなくても任意の文字列を表現できるので、表現できるものが減るとか増えるとかいうことはない。
Ruby には区切り文字を明示的に指定して文字列リテラルを記述する記法がある。
・ %!content! : ダブルクォーテーション区切りとほぼ同じ解釈 ・ %q!content! : シングルクォーテーション区切りとほぼ同じ解釈
上記で、! のところが区切りの指定である。上記のとおり ! を使ってもいいが、他の記号を使用しても良い。たとえば、%#content# などとも記述できる。
ここで、区切りが変わったことにより、中身の解釈が変わることがある。
シングルクォーテーション区切りの文字列で、区切り文字自体を表現するにはバックスラッシュを前置してシングルクォーテーションを記述する。
puts 'a\'a' => a'a
しかし、区切り文字自体がシングルクォーテーションでない場合は、シングルクォーテーション自体を表現するのにバックスラッシュを前置する必要はない。
puts %q|a'a| => a'a
というか、必要がないだけでなく、そうしてはならない。バックスラッシュを前置するとバックスラッシュとシングルクォートの 2文字になってしまう。
puts %q|a\'a| => a\'a
なお、このように区切りを | とした場合、| 自体を表現するにはもちろんバックスラッシュを前置する必要がある。
puts %q|a\|a| => a|a
むろん、シングルクォート区切りであれば、\| は \| の 2文字のままである。
puts 'a\|a' => a\|a
つまり、シングルクォート区切りおよび、%q では、実際の区切り文字によって、中身の解釈は変わる。これは悪いことではなく、なるべく書いたそのままに解釈するという意図の結果である。その結果として、エスケープがどうしても必要なバックスラッシュと区切り文字以外はそのまま書けるということが実現され、また、区切り文字を変えると、解釈が変わるという結果にもつながる。
さて、ここで区切り文字として \ を選ぶと、それでも動作する。
puts %q\abc\ => abc
ただし、エスケープシーケンスは記述できない。
puts %q\a\\c\ => -:1: syntax error, unexpected $undefined, expecting $end puts %q\a\\c\ ^
これは、エスケープシーケンス開始の \ と区切り文字の \ を区別できず、後者として解釈されるからである。
正規表現は、文字列との照合に使う。したがって、正規表現の中にはすべての文字を表記可能でなければならない。
Ruby は正規表現リテラルを持っている。正規表現はスラッシュで括り、たとえば、/a*/ などと記述してプログラムに埋め込むことができる。
そして、それに加えて正規表現特有の要素の記法が必要である。繰り返しとか、任意の一文字とか、行頭とか、グルーピングとか。
つまり、正規表現には文字列の照合のために任意の文字列を記述可能で、正規表現リテラルの終端検出が可能でなければならず、正規表現特有の要素も記述可能でなければならない。
そのために、文字列をベースとして、例によってバックスラッシュ倍増によって余裕を作る。
ここで、余裕ができたというのは、たとえば、* と \* を区別できるようになったという意味である。ダブルクォーテーション区切りの文字列では、" と \" を区別できることを利用して、前者を文字列の終端に用い、後者をダブルクォーテーション自体の表現に割り当てた。
正規表現においては、まず、ダブルクォーテーション区切りの文字列におけるエスケープシーケンスの大半をそのまま採用する。たとえば、f と \f を区別して別の意味を割り当てることが可能になるので、前者には f という文字自体の表現、後者にはフォームフィード文字の表現として割り当てる。
また、/ と \/ の区別により、前者を正規表現リテラルの終端に、後者をスラッシュ自体の表現に割り当てる。
また、* と \* の区別により、前者を繰り返しの指定に、後者をアスタリスク自体の表現に割り当てる。
一般に、どのような文字列でも記述可能であるように、ある文字 x 自体を表現する方法は常に必要なので、バックスラッシュのつかない x とバックスラッシュのついた \x のどちらかはその文字自体の表現に割り当てる。 Ruby (というか Perl) では、x が記号であれば \x がその記号自体の表現であり、 x がアルファベットであれば x がそのアルファベットの表現に割り当てられるという規則がある。いずれにしてももう一方を使って、文字自体とは異なる機能を実現する。
文字列と同様、正規表現リテラルの区切り文字を明示的に指定することもできる。その場合、%r!content! などと記述する。
x と \x が同じ意味を持つような x を区切り文字と指定するのであれば問題はないが、それぞれが異なる意味を持つ場合、 x の意味を区切り文字に指定すると奇妙なことになる。この問題は文字列の場合にもいくらかはあったが、正規表現ではそのような文字が多く、いろいろと奇妙な例を作れる。
たとえば、* を区切りとして使って %r*abc* という記述が可能である。しかし、* を区切りとすると、* による繰り返しは使えなくなる。
Ruby には、正規表現オブジェクトを正規表現リテラルの形で表示する機能がある。
p /abc/ => /abc/
ここで、正規表現オブジェクトがどのように生成されても、表示される正規表現リテラルはスラッシュ区切りである。
p %r|abc| => /abc/
ここで、正規表現にスラッシュが入っていると、以下のようになる。
p %r|a/c| => /a\/c/
つまり、スラッシュにバックスラッシュが前置され、エスケープされる。
もちろん、もともとバックスラッシュがついていればそのままである。
p %r|a\/c| => /a\/c/
また、スラッシュ以外のエスケープ状態は保存される。
p %r|a/c*d\*| => /a\/c*d\*/
この処理は、バックスラッシュ倍増状態において、/ と \/ が同じ意味であることによる。そして、終端としてスラッシュを選んだことにより、前者が単なる文字としては使用不能になり、後者に置換されるというわけである。
したがって、任意の正規表現オブジェクトを任意の区切りの正規表現リテラルに変換できるかというと、常に可能とは限らない。例えば、%r*...* の形にしようとしても、中に繰り返しの * が入っていれば、それは困難である。
Emacs や Python には正規表現リテラルは存在しないが、正規表現自体はそれなりに使われる。
ではどうやって正規表現を指定するかというと、文字列を使用する。
ここで問題になるのが、文字列自体とマッチする部分と、正規表現特有の要素をどう区別するか、というところである。
正規表現は文字列オブジェクトで表現され、文字列オブジェクトには、文字列リテラルでは区別可能であった * と \* の違いが残っていない。そのため、繰り返しとアスタリスク文字を区別するために、文字列オブジェクトのレベルでバックスラッシュ倍増を行って区別可能にする。
その結果、文字列オブジェクトのレベルと文字列リテラルのレベルの2段階でバックスラッシュ倍増が行われ、掛け合わされて 4倍増となり、バックスラッシュ自体とマッチする正規表現を表現するには、"\\\\" と 4つのバックスラッシュが必要となるのである。
HTML も、エスケープ記法を持っている。
HTML は、body とか p とかの要素をノードとする木構造であり、葉のテキストノードや属性などに文字列が表れる。
このような木構造をある文法でひとつの文字列にまとめたものが HTML ファイルであり、ここでもやはり HTML という文字列の中にテキストや属性といった文字列が埋め込まれる。
文字列を埋め込むからには終端の認識が必要で、終端自体は埋め込まれた文字列には含まれない必要があり、そのためには埋め込む文字列をエスケープしてそのあたりを空ける必要がある。
C言語の文字列ではバックスラッシュを倍増させて空きを作ったが、 HTML では、文字実体参照、とくに & を使用して空きを作る。具体的には文字列内における & の出現をすべて & に置換する。これによって、文字列内の & はすべて文字実体参照の一部としての出現となり、 & 以外のすべての文字は、その文字自体を使う表現と文字実体参照 (もしくは数値文字参照) による表現の 2種類をもてることになる。そこでそれぞれの意味を変えることにより文字列の終端を表現し、ひいては HTML という木構造を表現する。
HTML の木構造において、葉でないノードになるのは要素である。たとえばパラグラフの p 要素であり、...<p>...</p>... と記述することによって、 p 要素の兄弟と子供を区別する。ここで、兄弟や子供はテキストノードの可能性があり、テキストノードと隣接する要素との境目が確実に区別できるよう、アングルブラケット、つまり < と > は直接記述するのでなく、文字実体参照の < と > を使用する。
HTML の要素には属性を記述できる。属性には名前と値があり、開始タグの中に記述する。たとえば、p 要素には title という名前の属性を加えることができ、<p title="..."> というように書く。
ここでは属性値をダブルクォーテーションで括っている。これは、属性値自体にはダブルクォーテーションが直接は入れられないことを意味し、かわりに " を使うことになる。
HTML において文字列の類を記述できるのはテキストと属性だけではない。コメントにもかなり自由な記述ができる。
HTML のコメントは <!--...--> という形であるが、これもある文字列を HTML という文字列の中に埋め込む記法である以上、終端を検出する必要がある。コメントの終端は -- である。したがって、コメントの内容に -- を直接含めることはできない。
興味深いのは、コメントには -- を表現する記法が存在しないということである。この -- は単に終端であり、-- を表現する他の方法はない。
文字実体参照はコメント内には適用されない。これは文字実体参照が要素名などに使えないのと同じで、そういう規則なのである。
C言語においても、コメントの終了の */ をコメント内に記述する方法はない。これは、Cコンパイラがコメントを単に無視することから、だいたいにおいて場合問題にはならない。
HTML においてもブラウザは基本的にコメントを無視する。しかし、DOM を通せばコメントにもアクセス可能であり、また、RDF を書くとか、テンプレート展開の指示を書くとか、コメントが機械処理に使われているという状況がある。このため、-- が含められないという制約は実際に問題になることがある。
同様に、機械処理をするのであれば、C言語のコメントも問題になることがある。ドキュメントをコメントとして埋め込むことはよく行われるが、そこに */ を記述したいケースは考えられる。たとえば、正規表現の説明で /a*/ と書きたい、とかが考えられる。そうなれば同じく問題が生じる。機械処理を行う他の例としては、Emacs の file variable などもある。
ところで、コメントよりももっと面白いところで文字実体参照が使えないことがある。それは style 要素と script 要素の内容で、そこでは文字実体参照が使えない。
このあたりの話は HTML の規格では付録に書いてある。 http://www.w3.org/TR/html401/appendix/notes.html#h-B.3.2
これは、HTML の DTD で以下のように定義されているからである。
<!ENTITY % StyleSheet "CDATA" -- style sheet data --> <!ELEMENT STYLE - - %StyleSheet -- style info --> <!ENTITY % Script "CDATA" -- script expression --> <!ELEMENT SCRIPT - - %Script; -- script statements -->
ここで、ENTITY というのは実体の定義でマクロのようなものである。そこで StyleSheet と Script という実体を CDATA と定義している。
ELEMENT というのが要素の定義で、それを使って中身を CDATA と定義している。
これに対し、たとえば textarea 要素は以下のように定義される。
<!ELEMENT TEXTAREA - - (#PCDATA) -- multi-line text field -->
textarea の内容は、CDATA ではなく、#PCDATA と定義される。
この違いが文字実体参照が使えるかどうかの違いになる。 CDATA の場合は使えず、#PCDATA では使えるのである。 HTML ではほとんどのところで #PCDATA を使っているが、 style 要素と script 要素の内容だけは CDATA なのである。
このため、style 要素と script 要素の中に文字列を埋め込む場合、文字実体参照へエスケープするのは間違いとある。
さて、エスケープできないとすると、終端をどう表現するかが問題になる。エスケープしていないとすれば、どんな (固定の) 文字列を終端に使っても衝突してしまう。
で、どうなっているかというと、実際に衝突している。 <style>...</style> というので "</" という 2文字が終端を示すのであるが、中身に "</" を記述することはできないのである。
その対策は規格に書いてあるが、要するに、それはどうにか書かないで済ませ、ということである。
たとえば、JavaScript で
<script type="text/javascript"> document.write ("<em>emphasize</em>") </script>
と書きたいと思ったときは、ちょっと変えて、
<script type="text/javascript"> document.write ("<em>emphasize<\/em>") </script>
と書け、というのである。
なお、style と script の内容が CDATA であるというのは、XHTML ではなくなっている。
これは、XML では、テキストは常に #PCDATA であると決まっているからである。
XHTML は XML であるから、XML の決まりに従わなくてはならず、 XHTML の style と script は HTML のと違って、必要に応じて文字実体参照でエスケープしなければならない。この違いは XHTML の仕様でも触れられている。
URI にもエスケープの記法がある。パーセントエンコーディングと呼ばれる、%xx というやつで、16進で文字を記述できる。
CやRuby では、バックスラッシュの倍増によって空きを作っており、 HTML では & を & に変換することによって空きを作っている。
URI では、パーセント (%) を使用する。つまり、% を %25 に変換することによって空きを作る。これにより、パーセント以外の文字は、その文字を直接記述する表現と、パーセントエンコーディングされた表現の 2種類の表現をもつことになる。
このようにして (パーセント以外の文字については) ひとつの文字についてふたつの表現が可能になったので、どちらかをその文字自身を意味する表現とし、もう一方にそれ以外の機能を割り当てることが可能になる。
さて、ここで、「それ以外の機能」というのがどういうものか、というのが問題である。
CやRuby の文字列、あるいは HTML のテキストや属性では、文字列を文字列に埋め込む都合により終端を検出する必要があって、終端という機能を割り当てた。
Ruby の正規表現では、正規表現を文字列に埋め込む都合により、終端に加えて正規表現の機能を割り当てた。
それで URI は、というと、パーセントエンコーディングには終端の検出という意図はないように思える。
URI が他の文字列に埋め込まれる例としては HTML のリンクがある。つまり a 要素の href 属性であるが、 HTML の属性には任意の文字列を埋め込んで終端を検出する機能が (文字実体参照というエスケープ機構によって) 提供されているので、 URI がどんな文字列として表現されようが終端を検出しそこねることはない。
では、パーセントエンコーディングの目的は何か、というと、 URI の内部の構造で、その構造を表現するのに使っている文字を、構造とは関係なく使用するためにある。たとえば、http://host/path?query という URL では、 path と query は ? で区切られている。そのため、path には ? という文字を直接使うことはできない。しかし、ファイル名に ? が含まれていたとかの事情があり、どうしても使う必要があるときには、? に対応する %3F を使用するのである。つまり、パーセントエンコーディングは path の終端を検出できるようにしている。
というように、パーセントエンコーディングされた %xx は対応する文字自身を意味し、URI 内の区切りではない意味に割り当てられる。では、パーセントエンコーディングされていない文字はどのような意味を持つのか、というのが問題である。
このように、終端検出以外の意味を持つということは、 URI におけるエスケープ機構の位置づけは、 C言語の文字列というよりは、Ruby の正規表現に近い。
Ruby の正規表現には、正規表現エンジンの機能に対応する記法がある。
それなら URI ではどんな機能に対応する記法なのかというと、そこが難しい。正規表現では、具体的な正規表現エンジンがあって、機能を調べることができる。しかし、URI では、URI の仕様 (RFC:3986) 自体には個々の URI にどのような機能があるかということは規定されていない。そのかわりに、URI 全体に適用されるいくらかの制限が規定され、細かいところは scheme 毎に定義されるということになっている。
scheme 毎の仕様はどこを探せばいいかというと、 IANA で URI scheme の仕様のリストが管理されている。
たとえば、http については、このリストを見ると、RFC:2616 で定義される、ということがわかる。
しかし、それで終わりか、というとそうではない。ブラウザからフォームをサブミットするとき、フォームの内容をどのようにパーセントエンコーディングしてサーバに送るか、ということを規定する application/x-www-form-urlencoded は HTML の仕様の中で定義されている。
もちろん、http でない URI はまた別の仕様があるし、 http であってもフォームのサブミット以外の用途であれば application/x-www-form-urlencoded に従う必要はない。
URI の scheme は後から後から新しいのが提案されてそのたびに URI の仕様自体をいじるのは無謀だし、まして個々のアプリケーションがパーセントエンコーディングをどう使うかというのにあわせて URI の仕様を更新するのは非現実的である。
というわけで、URI の仕様自体は、個々の URI がどのような機能に対応するかを規定することはできないし、していない。ある構文を決めて、その意味は各 scheme の仕様に任せてしまうのである。そして、各 scheme の仕様が、その構文の要素に対して意味をつけたり、使わないとしたり、あるいは他の規格やアプリケーションの自由にまかすというわけである。
では、URI の仕様は何を規定しているかというと、エスケープの観点からするとまず以下のことを決めている。
・ 紙に書いたりディスプレイに表示するという要求を満たすため、(エスケープ済みの URI で) ASCII の表示可能文字以外は使わないと決定 ・ いくつかの不都合な文字も使わないと決定 ["], [<], [>], [\], [^], [`], [{], [|], [}] ・ エスケープにはパーセントエンコーディングを使うと決定 ・ unreserved と呼ばれるアルファベットと数字およびその他いくつかの記号 ([-], [.], [_], [~]) はパーセントエンコーディングしてもしなくても同じ意味であると決定
最初のは、日本語とかの問題はあるが、それは IRI (RFC:3987) の話になるのでここでは触れない。ともかく ASCII の表示可能文字だけを扱う。 ASCII の表示可能文字はコード順に ! から ~ までの 94 文字である。
次の、不都合な文字 9文字を使わないという決定は、RFC:3986 では理由は述べられていない。単に使用できる文字としてあげられていないだけである。これについては後で触れる。
そして、パーセントエンコーディングの決定により、 94 文字のうち、パーセント (%) の機能が決定する。このことは、パーセントという文字自体を表現したいときには必ず %25 を使用しなければならないということを意味する。
最後の unreserved はアルファベットの大文字小文字 26*2=52文字と、数字の 0 から 9 までの 10文字、ハイフン (-)、ドット (.)、アンダースコア (_)、チルダ (~) の計 52+10+4=66文字はパーセントエンコーディングしてもしなくても意味は変わらないということである。たとえば、チルダに対応する %7E を使った場合には常にチルダを生で書いても同じことになる。
というわけで、パーセントエスケープによって文字列の中にさまざまな機能を割り当てる余裕ができたのだが、使わない文字もそれなりにあり、パーセント自身はエスケープ以外には使えないし、 unreserved もその文字自身を表す以外には使わないと決定したので、 ASCII の表示可能文字 94文字のうち、残りの 94-9-1-66=18文字に機能を割り当てる余地がある。
この 18文字とは "!", "#", "$", "&", "'", "(", ")", "*", "+", ",", "/", ":", ";", "=", "?", "@", "[", "]" である。
["], [<], [>], [\], [^], [`], [{], [|], [}] という 9個の文字を不都合な文字として使わないとしたが、この理由は RFC:3987 には触れられていない。
しかし、以前の RFC:2396 やさらに前の RFC:1738 を読むと理由が浮かんでくる。 RFC:2396 には ["], [<], [>] の 3文字が、URI を区切るためによく用いられるから、と述べられている。たしかに、["] は HTML で使うし、文書の中ではアングルブラケットで URL を括ろうという提案も以前あった。
また、[\], [^], [`], [{], [|], [}] については、ゲートウェイや他の転送エージェントにより変化するかもしれない、という理由が述べられているが、具体的にどのゲートウェイが問題になるかということは述べられていない。さらに遡って RFC:1738 をみると、ゲートウェイが、という記述自体は同じであるが、 ABNF の非終端記号で、これらの文字に national という名前がついている。
national というので思い出すのは ISO 646 の各国版である。 ISO 646 は 94文字集合であるが、そのうち 82文字は全体で共通で、残りの 12文字をアメリカとか、日本とか、イギリスとか、各国で定義する。そして、national というのは、その 12文字の部分集合になっているのである。まぁ、12文字全部でないところが中途半端というかなんであるが。
unreserved というものが出てきたが、当然対応する reserved というものもある。 unreserved がパーセントエンコーディングしてもしなくても意味が同じ文字であるのに対し、 reserved はパーセントエンコーディングするとしないとで意味を変えても良い文字である。 (変えなくても良い。その選択は scheme の仕様にまかされている)
ただし、この unreserved という分類に含まれる文字は RFC:3986 と以前の RFC:2396 や RFC:1738 それぞれで微妙に異なっているので、将来的にもずっとこのままだという保証があるわけではない。
・ RFC:3986 Uniform Resource Identifier (URI): Generic Syntax ・ RFC:2732 Format for Literal IPv6 Addresses in URL's ・ RFC:2396 Uniform Resource Identifiers (URI): Generic Syntax ・ RFC:1808 Relative Uniform Resource Locators ・ RFC:1738 Uniform Resource Locators (URL)
それぞれで非終端記号はいろいろと違うが、 reserved と unreserved に関連するのは以下のとおりである。
RFC3986 reserved = gen-delims / sub-delims RFC2396 alpha = lowalpha | upalpha alphanum = alpha | digit unreserved = alphanum | mark RFC1738,1808 alpha = lowalpha | hialpha unreserved = alpha | digit | safe | extra
で、ASCII の記号についてどの非終端記号に対応するかを表にすると以下のようになる。 (r) がついているのが reserved で、(u) がついているのが unreserved である。ついでなので、ISO 646 の各国版で変わるかもしれないところも書いてある。
URI IPv6 URI URL
oct dec hex chr RFC 3986 RFC 2732 RFC 2396 RFC 1738,1808 041 33 21 ! sub-delims(r) mark(u) extra(u) 042 34 22 " delims punctuation 043 35 23 # gen-delims(r) delims punctuation ISO646可変部 044 36 24 $ sub-delims(r) reserved(r) reserved(r) safe(u) ISO646可変部 045 37 25 % delims punctuation 046 38 26 & sub-delims(r) reserved(r) reserved(r) reserved(r) 047 39 27 ' sub-delims(r) mark(u) extra(u) 050 40 28 ( sub-delims(r) mark(u) extra(u) 051 41 29 ) sub-delims(r) mark(u) extra(u) 052 42 2A * sub-delims(r) mark(u) extra(u) 053 43 2B + sub-delims(r) reserved(r) reserved(r) safe(u) 054 44 2C , sub-delims(r) reserved(r) reserved(r) extra(u) 055 45 2D - unreserved(u) mark(u) safe(u) 056 46 2E . unreserved(u) mark(u) safe(u) 057 47 2F / gen-delims(r) reserved(r) reserved(r) reserved(r) 072 58 3A : gen-delims(r) reserved(r) reserved(r) reserved(r) 073 59 3B ; sub-delims(r) reserved(r) reserved(r) reserved(r) 074 60 3C < delims punctuation 075 61 3D = sub-delims(r) reserved(r) reserved(r) reserved(r) 076 62 3E > delims punctuation 077 63 3F ? gen-delims(r) reserved(r) reserved(r) reserved(r) 100 64 40 @ gen-delims(r) reserved(r) reserved(r) reserved(r) ISO646可変部 133 91 5B [ gen-delims(r) reserved(r) unwise national ISO646可変部 134 92 5C \ unwise unwise national ISO646可変部 135 93 5D ] gen-delims(r) reserved(r) unwise national ISO646可変部 136 94 5E ^ unwise unwise national ISO646可変部 137 95 5F _ unreserved(u) mark(u) safe(u) 140 96 60 ` unwise unwise national ISO646可変部 173 123 7B { unwise unwise national ISO646可変部 174 124 7C | unwise unwise national ISO646可変部 175 125 7D } unwise unwise national ISO646可変部 176 126 7E ~ unreserved(u) mark(u) national ISO646可変部
あと、ここにあげてあるのは記号だけであるが、数字とアルファベットはいつも unreserved である。いちおう代表として 0, A, a だけあげておくと以下の通りである。
060 48 30 0 unreserved(u) digit(u) digit(u) 101 65 41 A unreserved(u) upalpha(u) hialpha(u) 141 97 61 a unreserved(u) lowalpha(u) lowalpha(u)
さて、それぞれの RFC で各文字は reserved であったり unreserved であったりそのどちらでもなかったりするわけであるが、 RFC 間でそれが変わっている文字がある。
URI IPv6 URI URL
oct dec hex chr RFC 3986 RFC 2732 RFC 2396 RFC 1738,1808 041 33 21 ! sub-delims(r) mark(u) extra(u) 043 35 23 # gen-delims(r) delims punctuation ISO646可変部 044 36 24 $ sub-delims(r) reserved(r) reserved(r) safe(u) ISO646可変部 047 39 27 ' sub-delims(r) mark(u) extra(u) 050 40 28 ( sub-delims(r) mark(u) extra(u) 051 41 29 ) sub-delims(r) mark(u) extra(u) 052 42 2A * sub-delims(r) mark(u) extra(u) 053 43 2B + sub-delims(r) reserved(r) reserved(r) safe(u) 054 44 2C , sub-delims(r) reserved(r) reserved(r) extra(u) 133 91 5B [ gen-delims(r) reserved(r) unwise national ISO646可変部 135 93 5D ] gen-delims(r) reserved(r) unwise national ISO646可変部 176 126 7E ~ unreserved(u) mark(u) national ISO646可変部
たとえば、! は RFC 2732 までは unreserved であったが、RFC 3986 では reserved になっている。 (RFC 2732 は IPv6 の追加だけなので、記載されていないところは RFC 2396 に等しい。) つまり、昔は %21 は ! と等価であることが保証されていたが、今は保証されていない、ということである。
また、# は RFC 2732 までは URI の一部として認められていなかったが、RFC 3986 では reserved になっている。まぁ、これは RFC 3986 から fragment が URI の一部として扱われるようになったという関係の話で、それ以上の話ではない。
ブラケットは RFC 2396 までは URI に使えなかったが、RFC 2732 で reserved となった。これは http://[1080::8:800:200C:417A]/ などといった IPv6 な URI を扱うためで、それ以外に使えるわけではない。
あと、ほとんどの文字が reserved へ変化している中で、唯一 unreserved に変化している文字が ~ である。これは RFC 1738 では national であって URI には使えなかったが、RFC 2396 からは unreserved であり、 %7E と ~ は等価になっている。これは、http://host/~user の用法を追認したという話であろう。
これらの変化を見ると、~ という例外はあるものの、reserved が拡大傾向にある。 unreserved が reserved に変化することも多いし、 URI に使えなかった文字が reserved に変化することもある。いままでパーセントエンコーディングしてもしなくても同じと保証されていた文字が、その保証がなくなるというわけである。つまり、そんな保証は信用しないのが見識である。 URI の正規化とかいって、可能な範囲でパーセントエンコーディングを解くというのはやめておいたほうがいいであろう。
URI の構文規則は以下の通りである
URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
これを、非終端記号が文字列っぽいものになるまで適当に展開していくと次のようになる。 (ここで、path-absolute, path-rootless, path-empty, IP-literal, IPv4address の可能性は無視している)
URI = scheme ":" "//" [ userinfo "@" ] reg-name [ ":" port ] *( "/" segment ) [ "?" query ] [ "#" fragment ]
ここで残っている非終端記号は以下の通りである。これらはどれもほぼ文字の繰り返しという形で定義されており、文字の並びという意味でなんとも文字列っぽい。
scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) reg-name = *( unreserved / pct-encoded / sub-delims ) port = *DIGIT segment = *pchar query = *( pchar / "/" / "?" ) fragment = *( pchar / "/" / "?" ) pchar = unreserved / pct-encoded / sub-delims / ":" / "@" pct-encoded = "%" HEXDIG HEXDIG
これをみてわかるのは、まず、scheme, port はパーセントエンコーディングが使えないということである。 port で使えないのは、整数を指定するのにそんなものを使っても意味がないというところであろう。 scheme で使えないのはそれにくらべると微妙だが、せめて scheme くらいはわけのわからない名前は付けさせないという宣言のようにも思える。
残りの userinfo, reg-name, segment, query, fragment ではパーセントエンコーディングが使用できる。エスケープの観点から興味深いのはこれらである。 pchar も展開するとそれらは以下のようになる。
userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) reg-name = *( unreserved / pct-encoded / sub-delims ) segment = *( unreserved / pct-encoded / sub-delims / ":" / "@" ) query = *( unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?" ) fragment = *( unreserved / pct-encoded / sub-delims / ":" / "@" / "/" / "?" )
つまり、unreserved, pct-encoded, sub-delims は共通である。 unreserved は特別な機能を持たせないことが保証されているのでそれが常に使えるというのは当然である。 pct-encoded はパーセントエンコーディングにより任意の文字列を表現するのに必要であるからこれも必然である。 sub-delims は reserved の一種で、(URI の本体仕様でなく) 各 scheme の仕様で機能を持たせることができるというものであるから、 URI の本体仕様自身では制約を設けず使えるようにするは理解できる。
残りの、他に何文字か使用可能な文字が追加されているのがなんとも ad hoc に思える。これらはどんな理由によって使用可能であると決定されたのだろうか。
ここで、追加されている 4文字の ":", "@", "/", "?" と URI の構文を見比べると、これらの文字が区切りに使われていることがわかる。
URI = scheme ":" "//" [ userinfo "@" ] reg-name [ ":" port ] *( "/" segment ) [ "?" query ] [ "#" fragment ]
":" は port の直前の区切りであり、 "@" は userinfo の直後の区切りであり、 "/" は segment の直前の区切りであり、 "?" は query の区切りである。
そういう区切り文字を使ってもいいとすると、区切りを曖昧にするという可能性が出てくる。しかし、個々に考えてみると、これらは曖昧にならないように選ばれていることがわかる。
・ userinfo には ":" が使えるが、port 直前の ":" とは "@" で区切られているので、解釈が曖昧になる可能性はない ・ segment には ":" と "@" が使えるが、それらは segment 以降で区切りとして使われていないので曖昧にはならない ・ query には ":", "@", "/", "?" が使えるが、それらは query 以降で区切りとして使われていないので曖昧にはならない ・ fragment には ":", "@", "/", "?" が使えるが、それらは fragment 以降で区切りとして使われていないので曖昧にはならない
逆に、4文字のうち、それぞれで追加されていない文字は多くの場合構文を曖昧にしてしまう。
・ userinfo で "/" が使えてしまったら、scheme://user/info@reg-name となって、user が reg-name, info@reg-name が segment という解釈が可能になる ・ userinfo で "?" が使えてしまったら、scheme://user?info@reg-name となって、user が reg-name, info@reg-name が query という解釈が可能になる ・ reg-name で ":" が使えてしまったら、scheme://reg:name となって、reg が reg-name, name が port という解釈が可能になる ・ reg-name で "@" が使えてしまったら、scheme://reg@name となって、reg が userinfo, name が reg-name という解釈が可能になる ・ reg-name で "/" が使えてしまったら、scheme://reg/name となって、reg が reg-name, name が segment という解釈が可能になる ・ reg-name で "?" が使えてしまったら、scheme://reg?name となって、reg が reg-name, name が query という解釈が可能になる ・ segment で "/" が使えてしまったら、scheme://reg-name/seg/ment となって、seg と ment というふたつの segment があるという解釈が可能になる ・ segment で "?" が使えてしまったら、scheme://reg-name/seg?ment となって、seg が segment で ment が query という解釈が可能になる
このように検討すると、userinfo か reg-name で "@" が使えてもいいのではないかということが思い浮かぶ。現在は userinfo, reg-name は両方 "@" を使えないが、最初の "@" で区切るか最後の "@" で区切るかを定義すれば、 scheme://user@info_or_reg@name のようなものも曖昧さなく解釈できる。とはいえ、userinfo はあまり使われないし、reg-name はドメイン名の都合で制約があるから、許可してもあまりうれしくないか。
もうひとつの疑問は、fragment における "#" である。 fragment には "#" を許してもいいように思える。まぁ、fragment は http ではサーバに送られないのであまり処理する機会もなくて、どっちでもいいといえばどっちでもいいが。
ここまでで検討した reserved は、":", "@", "/", "?", "#" の 5文字に加え、 sub-delims の 11文字である。これで合計 16文字であるが、 reserved は 18文字あったはずである。
残りの 2文字は、ということで調べてみるとそれは "[", "]" である。
つまり、パーセントエンコーディングされているのとされていないので別の意味を割り当てられるというのが reserved のはずなのに、userinfo, reg-name, segment, query, fragment では生でブラケットを記述することができず、意味を割り当てようがないという奇妙な状況になっている。
ここで、ブラケットは IPv6 のために RFC 2732 で reserved に加わったが、 RFC 2732 は RFC 2396 に対する変更という立場である。とすると、もしかしたら、RFC 2396 では reserved という非終端記号を使っていたのかもしれない。ということで調べてみると、query と fragment が reserved を使って定義されていた。
query = *uric fragment = *uric uric = reserved | unreserved | escaped
しかし、userinfo, segment の定義では使っていなかった。
userinfo = *( unreserved | escaped | ";" | ":" | "&" | "=" | "+" | "$" | "," ) segment = *pchar *( ";" param ) param = *pchar pchar = unreserved | escaped | ":" | "@" | "&" | "=" | "+" | "$" | ","
このような定義で reserved にブラケットを加えたとすると、 query と fragment にはブラケットが使えるようになるが、 userinfo と segment では使えないままである。
まぁ、あんまり意図的な感じはしない。
そして、RFC 3986 では、また query と fragment でブラケットが使えなくなっている。でも、reserved には残っているわけである。
これをどう考えるかというと、まぁ、なんとも怪しげである。
URI の仕様本体の RFC 3986 では個々の scheme にいろいろと仕様を任せているが、 http について調べてみよう。
http URI は RFC 2616 の 3.2 節に定義されている。目次を見ても、3ページ弱しかない。
3.2 Uniform Resource Identifiers ................................18 3.2.1 General Syntax ...........................................19 3.2.2 http URL .................................................19 3.2.3 URI Comparison ...........................................20
中身は、というと、やはりエスケープについてたいしたことは書いていない。
まったく書いていないわけではないが、 reserved と unsafe 以外の文字はパーセントエンコーディングしてもしなくても同じ、というだけである。
しかし、reserved についてのその性質は URI の仕様で既に述べられているし、 unsafe というのは RFC 2616 が参照している URI の仕様である RFC 2396 には規定されていないのである。さらに古い URL の仕様である RFC 1738 であれば書いてあるが、その unsafe はそもそも URL に生では使えない文字なので、そのことについて触れる必要があるのか疑問である。
つまり、URI はリクエストの Request-URI でクライアントからサーバに伝えられるという以上には規定されておらず、 query でパーセントエンコーディングされているかどうかでどう意味が変わるかなどといったことは決まっていない。
エスケープ以外のことでいえば、以下の構文が載っていて、userinfo がどこにもないことがわかる、というのはそれなりに興味深い。
http_URL = "http:" "//" host [ ":" port ] [ abs_path [ "?" query ]]
あと、abs_path が空の URL は abs_path が "/" の URL と等しい、ということが述べられている。これは http というプロトコル特有の事情なので、そういう事情が URI の仕様に加えて定義されるという例として面白い。
http の規格でも http URL におけるパーセントエンコーディングの有無による意味の違いは定義されなかった。とすると、その意味は http を利用する側で約束するしかない。
そういう約束のひとつが HTML で定義される application/x-www-form-urlencoded である。
これは、ブラウザが HTML のフォームをサブミットするときにフォームの内容を文字列にエンコードする方法のひとつである。 method が GET であれば URL の query になるし、 POST であれば http request の body として送られる。
ここで、フォームの内容とはなにかというと、コントロール名と値の対の並びである。コントロール名と値はそれぞれ文字列である。 application/x-www-form-urlencoded はそれらをまとめてひとつにする。というわけで、ここでも Ruby の正規表現や URI 自体と同じく文字列ではないものをひとつにまとめる処理が出てきた。
しかし、正規表現や URI 自体と違うのは、まとめた結果が任意の文字列とはちょっと違うということである。文字列ではあるのだが、URI の query にならないといけないので、query の文法に適合しないといけない。
query の文法の sub-delims を展開すると以下のようになる。
query = *( unreserved / pct-encoded / "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" / ":" / "@" / "/" / "?" )
というわけで、使用できる記号は結構いろいろあるわけだが、 application/x-www-form-urlencoded では "&", "+", "=" を使う。
"+" はスペースの置換として使う。別に %20 でもいいはずだが、エンコード済みのものを人間が読みやすくなることを狙ったものだろうか。それはそれとして、これは "+" にスペースという意味を与えたことになる。その結果、"+" という意味を表現するには "+" は使えなくなり、%2B を使うことになる。
そして、"&" と "=" を使って、対の並びを組み上げる。コントロール名を k1, k2, ... としそれぞれに対応する値を v1, v2, ... とすると、 K1=V1&K2=V2&... とまとめるわけである。ここで K1, K2, ... および V1, V2, ... は区切りの "=" と "&" と混同しないようにコントロール名と値をパーセントエンコーディングしたものである。これは "&" に対と対の区切りという意味を与え、"=" にコントロール名と値の区切りという意味を与えたことになる。
また、フォームからサブミットするのではなく、 HTML の属性で URI を書くときには "&" のかわりに ";" を使うという話があるので、 ";" を生で使うと対と対の区切りとみなされてしまうかもしれない。そこで、";" もパーセントエンコーディングする必要がある。これは ";" に対と対の区切りという意味を見出すプログラムへの対応である。
ここで、K1=V1&K2=V2&... というのを曖昧さなくデコードできるというだけの条件であれば、 "%", "&", "=", "+", ";" の 5文字だけをパーセントエンコーディングすれば良い。これらの文字は application/x-www-form-urlencoded という仕様で、パーセントエンコーディングの有無で異なる意味を割り当てられたわけである。
しかし、query の文法にあわせるには、それだけでなく、query の文法に含まれていない文字もパーセントエンコーディングしなければならない。
さて逆に、パーセントエンコーディングしなくてもいい文字は、 query で (生で) 使える文字から "&", "=", "+", ";" を除いたものである。 application/x-www-form-urlencoded を実装するときにこういう文字集合の引き算ができると便利であるが、そういう引き算ができる正規表現エンジンは一般的でないのが残念ではある。
具体的に引き算の結果をリストアップすると、以下の文字が application/x-www-form-urlencoded のコントロール名や値においてパーセントエンコーディングしなくていい文字である。これら以外はパーセントエンコーディングしなければならない。
ALPHA / DIGIT / "-" / "." / "_" / "~"
/ "!" / "$" / "'" / "(" / ")" / "*" / "," / ":" / "@" / "/" / "?"
ともあれ、そうやってひとつにまとめられたフォームの情報は、 GET リクエストの Request-URI に追加されるか、 POST リクエストの body を通じて HTTP サーバ側に到達する。
HTTP サーバ側において application/x-www-form-urlencoded なものが処理される場所のひとつは CGI プログラムの中である。
HTTP サーバは、受け取ったリクエストの処理を CGI にまかせるとした場合、 Request-URI の query 部分を QUERY_STRING 環境変数によって CGI プログラムに渡す。
CGI プログラムで、もともとのフォームのコントロール名と値を得るには、 QUERY_STRING を application/x-www-form-urlencoded なものだとしてデコードしなければならない。そのようなコードを書いたひとは結構多いのではないだろうか。
application/x-www-form-urlencoded はコントロール名と値の対の並びで、コントロール名と値は文字列であった。
さて、パーセントエンコーディングしなくていい文字は以下のとおりで、 reserved な文字はまだまだ残っている。
unreserved / "!" / "$" / "'" / "(" / ")" / "*" / "," / ":" / "@" / "/" / "?"
つまり、コントロール名や値を、文字列ではない、なにか他のものを表現するための余地が残っているということである。
たとえば、Ruby みたいに、先頭が ":" だったら文字列じゃなくてシンボル、というのはどうだろう?
まぁ、ブラウザをいじるのは面倒臭いし普及するとも思えないので、非現実的ではある。 URL を生成して HTML に埋め込むならまだあるかもしれないが、本当にそういうことをしたいのかというと疑わしい。
だが、URI のパーセントエンコーディングという仕掛けが reserved な文字にどんどん意味を割り当てていって、それにつれてパーセントエンコーディングすべき文字集合が変わっていくというシステムである以上、エスケープ・アンエスケープを支援するライブラリはそういう拡張をサポートするべきかもしれない。などといっても実際に必要かというと疑わしいか。
ところで、ときに URI に URI を埋め込むことがある。
URI を文字列とみなした上で必要なだけパーセントエンコーディングして埋め込むのは簡単である。しかし、そうするとパーセントエンコーディングが増えて見苦しいので、そのまま埋め込みたいと考えてもおかしくない。
URI のどこに埋め込むかは選択肢があるが、query に埋め込むことを考える。
ここで問題になる文字が、"#", "[", "]" である。
まず、query には "#" を記述できないので、 fragment をもつ URI をそのまま埋め込むことは無理である。 "#" は query と fragment の区切りに使われているので、これはどうしようもない。
また、"[", "]" も (怪しい話だが) 記述できないので、 IPv6 な URI をそのまま埋め込むことも無理である。ただ、ブラケットが使えないのは怪しい話なので、使えるように行動を起こすとか、あるいは RFC 2732 だと言い張れば使えるかもしれない。
ここで、fragment と IPv6 をあきらめると、それ以外で必要な文字は query にすべて記述できるのでそのまま埋め込むことができる。
このように埋め込んだとき、 query 内の文字のパーセントエンコーディングの有無の意味の違いは、埋め込んだ URI におけるその意味の違いに等しくなる。これは、application/x-www-form-urlencoded とは違った意味を reserved な文字に与える例となる。
IANA によれば、URI の ftp scheme は RFC:1738 で定義される。
RFC 1738 は URI の初期の定義で、ftp とか http とかいくつかの scheme の定義を含んでいる。
ただ、RFC Index によれば、これはすでに obsoleted なのだが、それは気にしないことにしよう。
さて、ftp URL は以下の形式である。省略可能な部分もあるがそれも気にしない。
ftp://<user>:<password>@<host>:<port>/<cwd1>/<cwd2>/.../<cwdN>/<name>;type=<typecode>
ここで、<cwd?> と <name> においては "/" と ";" は reserved と述べられており、それらを <cwd?> と <name> の内部で使うにはパーセントエンコーディングしなければならない。
ここで面白いのは <name>;type=<typecode> の部分である。
この部分は RFC 3986 の URI の文法では最後の segment に対応する。ひとつの segment の中に、<name> と <typecode> を埋め込むために、 ";" を区切りとして使っている。これは、segment の中で ";" に区切りとしての意味を割り当てたとみなせる。そのために <name> の中の ";" をパーセントエンコーディングしなければならないことになったのである。
しかし、"type=" の部分はどうだろうか。アルファベットは unreserved であるから、"type" はパーセントエンコーディングをしてもしなくても意味が変わらないはずで、 "%74y%70e" などと書いてもいいはずである。
まぁ、実装がそうなっているかは別の話で、Ruby の uri ライブラリはそうなっていない。
% /usr/bin/ruby -v -ruri -e 'p URI("ftp://host/file;%74y%70e=a").typecode' ruby 1.8.5 (2006-08-25) [i486-linux] nil
あと、"=" は URI では reserved (sub-delims) なので、意味が変わってもおかしくない。でも、RFC 1738 では reserved とは書いてない。でもでも、見かけからいうと区切りに見える。でもでもでも、認識した ";" の直後に "type=" がくるというルールなのでパーセントエンコーディングされていても認識することはできる。
さて、どうなのかな。
まぁ、規格を眺めても書いていないことは不明なので、どうだったら幸せになれるかということを考えると、 type のようなパラメータが将来増える可能性を考慮すると、 "=" も区切りとしたほうがいいような気がする。
さて、<typecode> は "a", "i", "d" のいずれかである。 "d" というのはディレクトリを示すのでちょっと特別扱いされるが、それ以外であればその URI が参照するファイルを取得するのは以下のような動作となる。
・ <host> の <port> に接続し ・ <user> と <password> で認証して ・ CWD <cwd1> を送り、 ・ CWD <cwd2> を送り、 ・ ... ・ CWD <cwdN> を送り、 ・ TYPE <typecode> を送り、 ・ RETR <name> でファイルを取得する
ここで、ftp サーバにつないだり送ったりするものは、すべてパーセントエンコーディングをデコードした形で行う。
ftp URL が指すファイルというのは、上記のようにコマンドを発行してアクセスできるもの、というのが定義である。もちろん、その定義と同じファイルをアクセスできるのであれば他のコマンドを送ってもいいのだが、相手が非 Unix 環境 (VMS とか?) の可能性を考慮すると一般には難しい。
ところで、ftp というプロトコルにデータを載せる前にパーセントエンコーディングをデコードするのは、考えてみればあたりまえである。 ftp というプロトコルははパーセントエンコーディングが考案される前からあるわけで、サーバ側でパーセントエンコーディングを処理することはありえない。
また、URI に出現する区切りは ftp プロトコルの中には現れない。もちろん、ftp://example.org/abc/def/ghi と ftp://example.org/abcd/ef/ghi の区切り文字の位置の違いは CWD の引数の違いを生み出すが、区切り文字の "/" 自体は CWD の引数やその他プロトコル内で使われたりはしないということである。これは、ftp URI の区切り文字が、データというよりは指示としての意味を持っているともみなせる。
なお、これに対し、http では、区切り文字やエスケープをそのまま Request-URI としてサーバへ送る。これは、あからさまに区切り文字を含めてデータであり、その中身には関知しないという雰囲気である。
ftp では http と違って、パーセントエンコーディングがデコードされた形でサーバ側に伝わるため、パーセントエンコーディングの有無の意味は純粋にクライアント側で決定される。つまり、サーバサイドアプリケーションが解釈を変えるということができないので、意味のぶれは少ないことが期待できる。
一般に、プロトコルでパーセントエンコーディングされた形を扱わないのであれば、同様になるので、古いプロトコルはみんなそうなのではないだろうか。
ftp URL において segment はパーセントエンコーディングがデコードされて CWD の引数に指定される。つまり、segment に %2F を含めると、CWD の引数に "/" を含めることができる。
これは意図的な仕様であり、 RFC 1738 にはそのことが例をあげて説明されている。
これはとくに、ftp でログインした時点での (リモートの) カレントディレクトリがルートディレクトリでないというケースで重要になる。 anonymous ftp ではあまりないかもしれないが、 anonymous でない場合にはログイン直後のカレントディレクトリは普通ホームディレクトリであってルートディレクトリではない。
そのような場合、絶対パスでファイルを指定したければ、ftp://example.org/%2Fdir/name などとして、絶対パスの /dir で CWD する必要がある。カレントディレクトリがルートディレクトリであれば ftp://example.org/%2Fdir/name と ftp://example.org/dir/name は同じであるが、そうでなければ違うのである。
なお、ここで、"/" で始まるのがルートディレクトリを示すというのは Unix のお約束なので、この URL が絶対パスというのは、相手が Unix であることを前提としている。
また、ftp://example.org/foo%2Fbar/ などとできるので、 URI の階層構造と、ftp でアクセスされる先のファイルシステムの階層構造はかならずしも一致しない。
URI の階層構造は相対 URI の解決に使われる。そのため、.. の意味がファイルシステムにおける意味とは異なる可能性が出てくる。たとえば、../../foo という相対 URI を ftp://example.org/%2Fdir/%2Fdir/%2Fdir/bar をベース URI として解決すると、 ftp://example.org/%2Fdir/foo になる。でも、(相手が Unix であれば) ベース URI が参照するファイルは /dir/bar であり、相対 URI が参照するファイルは /dir/foo であって、同じディレクトリのファイルになる。
ところで、RFC 1738 には、空の segment の場合の動作も書いてある。たとえば、ftp://example.org//foo/bar というので "CWD ", "CWD foo" とするというように、空の segment に対応して空の引数で CWD するのだそうな。
しかし、RFC:959 では以下のように引数を空にするのは許されていないと思うのだが、これはどういうことであろうか
CWD <SP> <pathname> <CRLF> <pathname> ::= <string> <string> ::= <char> | <char><string>
もしかして、空の引数に対しても動作するのがデファクトスタンダードだったりする?
パーセントエンコーディングにより、ftp URL の <cwd?> や <name> では任意の文字列を表現できる。しかし、ftp というプロトコルの都合として、受け付けられない文字列がある。
CWD <SP> <pathname> <CRLF> RETR <SP> <pathname> <CRLF> <pathname> ::= <string> <string> ::= <char> | <char><string> <char> ::= any of the 128 ASCII characters except <CR> and <LF>
つまり、CR と LF を含む文字列は受け付けられない。
これは、CWD とかのコマンドで引数の終端を検出するための制約である。
もし、ftp というプロトコル自身が <pathname> のところに任意の文字列を埋め込むためのエスケープ機構を提供していればそういう制約はなくなるのだが、提供しないので表現可能な文字列自体に制約がかかるわけである。
Unix の場合、ファイル名に CR や LF を使うことが可能なので、この制約は実際に不都合を生じている。
なお、終端は CRLF なので、単独の CR と LF は CR LF という連続でないかぎり許しても終端の検出には不都合を生じないはずだが、そうしていないのはいろいろと厄介だからであろう。すくなくとも文法を定義するのが面倒だし、そうやったとしても不都合がなくなるわけでもない。
しかし、それはそれとしてそういう (些細な) 不都合が許されるるのだとすれば、各 OS に依存しない抽象的なファイルシステム (というかファイル名) を定義して、プロトコルの中ではそれを使えばいいのに、と思わないでもない。
まぁ、あえてそれをしないのが IETF 流、という気もするが。
ここ何日か、CODE blog でエスケープについて書いているのだが、なかなか終らない。
あらためて書いてみて気がついたのだが、正規表現のエスケープと URI のエスケープが、文字列以外のものを表現しているという意味で共通していることに気がついたのは発見であった。
こういう、文字列以外のものを表現しているエスケープは、アンエスケープできない。
そういえば、端末のエスケープシーケンスもアンエスケープできないな。
ちょっとネタを変えて、スラッシュまみれでないものをみてみよう。
mailto URL は RFC:2368 で定義されている。
これはたとえば、mailto:chris@example.com みたいなやつである。
また、ヘッダとかボディを query につけることもできて、 mailto:joe@example.com?cc=bob@example.com&body=hello というようなこともかける。 query の内部は application/x-www-form-urlencoded にすごく似ている。 (application/x-www-form-urlencoded という言葉はどこにも出てこないし、 HTML の仕様も参照していないけれど)
さて、RFC:2368 では、mailto URL の構文は以下のように記載されている。
mailtoURL = "mailto:" [ to ] [ headers ] to = #mailbox headers = "?" header *( "&" header ) header = hname "=" hvalue hname = *urlc hvalue = *urlc
ここで、to の #mailbox は、RFC 822 の mailbox の文法に従う文字列をパーセントエンコーディングしたもの、とされている。どの文字をパーセントエンコーディングするかは、「all URL reserved characters in "to" must be encoded」となっていて、reserved 全部、らしい。
まぁ、#mailbox という独自の記法を使うのは ABNF (RFC:2234)に対して素人くさい。こういうときは prose-val であろう。<mailbox in RFC 822. percent-encoded> とか。
また、urlc は奇妙である。
RFC:2368 は urlc は定義されていないし、参照されている URI の仕様である RFC:1738 にも定義されていないのである。
その後更新された URI の仕様である RFC:2396 には、 urlc はないが、類似した名前の uric がある。
uric = reserved | unreserved | escaped
まぁ、RFC:2396 のドラフトあたりを参照してしまって、そこで名前が違っていたのだ、というような事情を推察できないわけではないが、ちょっとナニである。
それはそれとして、「As with "to", all URL reserved characters must be encoded」という記載があり、hname, hvalue でも reserved は全部パーセントエンコーディングしなければならない。
しかし、それなら hname, hvalue に reserved を含める必要はないわけで、
urlc = unreserved | escaped
というように uric から reserved を抜いて定義したほうがいいんではないかと思うわけだが、そうはなっていない。まぁ、次に述べるように reserved というのはいったい何なのか、という問題があるので含めておいたほうがいいという可能性もあるが。
RFC:2368 に載っている例で、mailto:chris@example.com というのがあるように、 "@" はパーセントエンコーディングしなくてもよいというのが意図であると理解できる。
しかし、"@" という文字は、reserved である。 RFC:1738 にも以下のように定義されている。
reserved = ";" | "/" | "?" | ":" | "@" | "&" | "="
ただ、文章では、「The characters ";", "/", "?", ":", "@", "=" and "&" are the characters which may be reserved for special meaning within a scheme.」という記述があり、各 scheme で "@" を reserved としてもよい、ともとれる。そうしてもよいとすれば、そうしなくてもよいということで、 mailto scheme の定義の立場では、そうしない、という決定を下したとも考えられる。 RFC:2396 には「Characters in the "reserved" set are not reserved in all contexts.」という記述もあり、そういう解釈が正しいのかもしれない。
つまりここでの問題は、 mailto URL においてエスケープしなければならないとされた「all URL reserved characters」というのがなんであるか、という点である。
これを URI の仕様で定められる reserved という非終端記号に含まれる文字、と解釈すると、"@" をエスケープしなければならないことになるので、 mailto の仕様に記載されている例が間違っていることになる。
もうひとつは、実際に区切り文字として利用されると決定を下した文字という解釈である。この解釈をとれば、"@" はエスケープしなくてもよいので例が間違っていない。しかし、どの文字が区切り文字として採用されたか、というのが難しい。
RFC:2368 では「Within mailto URLs, the characters "?", "=", "&" are reserved.」とあるので、この 3文字が reserved というのは比較的明らかである。
しかし、それでも、区切りが曖昧にならないという条件だけを考えれば、 to に "=", "&" を使っても問題は起きないし、 hname, hvalue に "?" を使うことも問題ない。
また、mailto 固有でなく、URI 一般の構文内での reserved はどの文字か、というのも明確には書いていないので判断に悩む。
mailtoURL と RFC 3986 の URI を比較してみよう。
mailtoURL = "mailto:" [ to ] [ "?" header *( "&" header ) ] URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
これらを比較すると、mailto の to と hier-part が対応することがわかる。
hier-part は以下のように定義される
hier-part = "//" authority path-abempty / path-absolute / path-rootless / path-empty
これらの中で、"/" で始まらない (かつ空でない) のは path-rootless で、 mailto の URL は普通はそれに対応すると考えられる。
path-rootless = segment-nz *( "/" segment ) segment-nz = 1*pchar segment = *pchar pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
しかし、mailbox には "/" が出てきてもおかしくはない。 "/" は RFC 822 において specials に入っていないので atom に使える。 atom に使えるということは local-part や phrase の先頭に使えるということで、つまりは mailbox の先頭に使えるということである。
ここで、//i/love/slash/@example.org というメールアドレスが仮に存在したとしたら、 mailto://i/love/slash/@example.org という mailto URL を記述できるだろうか? それとも、mailto:%2F%2Fi%2Flove%2Fslash%2F@example.org としなければならないだろうか?
ここでまた「all URL reserved characters」というのがなんであるか、という点が問題になる。
reserved というのはパーセントエンコーディングの有無で違う意味を割り当てられる文字のことである。そして、"/" は URI において階層構造を表す。しかし、メールアドレスに階層構造はない。ということは、"/" をその文字自身の意味として割り当ててもメールアドレスの観点からは問題なさそうである。
ただ、URI の階層構造は相対 URI によって使われているので、そういうように意味を割り当て直すのは相対 URI に影響が出そうである。
mailto では相対 URI なんて使わないといえばそうなのだが、規格の位置づけ上 URI の仕様自体に入っているのでしょうがないとしてそれを考慮するなら、%2F を使わざるを得ないであろう。
さて、"/" を使えないとすると、mailtoURL の to に対応するのは segment-nz になる。これは 1文字以上の長さの segment に等しい。
segment-nz の内部で使えるのは pchar すなわち unreserved / pct-encoded / sub-delims / ":" / "@" で、 "@" が使える。
ここで区切り文字がなにかというと、pchar に入っていない文字が区切りとなる。それを mailto の「all URL reserved characters」だと思えば、結局、to においてパーセントエンコーディングしなければならないのは pchar に入っていない文字である。
同様の考え方でいけば、 headers の hname, hvalue でパーセントエンコーディングしなくていいのは application/x-www-form-urlencoded とほぼ同じで、 query に使える文字から "=", "&" を抜いたものになる。それは具体的には以下である。
ALPHA / DIGIT / "-" / "." / "_" / "~" / "!" / "$" / "'" / "(" / ")" / "*" / "+" / "," / ";" / ":" / "@" / "/" / "?"
mailto の headers と application/x-www-form-urlencoded は "&" と "=" で区切る名前と値の対の並びということで、とても似ているが微妙に違う。
具体的には "+" と ";" で、 mailto ではそれらに特別な意味を割り当てておらず、"+" と ";" という文字自身をあらわす。しかし、これらは application/x-www-form-urlencoded において、スペースと区切りの意味がある。
したがって、application/x-www-form-urlencoded 用のエンコーダで、スペースが "%20" になり、区切りに "&" を使うものであれば、headers 用のエンコーダとしても利用できる。
なお、application/x-www-form-urlencoded 用のデコーダを (無理に) 利用してしまうプログラムへの対策を考えるならば、 headers 用のエンコーダで "+" を "%2B"、";" を "%3B" にパーセントエンコーディングすることも考えられる。
ここで、スペースを "%20", "+" を "%2B", ";" を "%3B" にして、区切りに "&" を使うエンコーダは headers と application/x-www-form-urlencoded の違いを考えなくてよい。
このように両方に使えるエンコーダはプログラマが使い分ける必要がないので使い易いが、生成された URL でパーセントエンコーディングが増えてしまうのが問題ではある。
あと、headers と application/x-www-form-urlencoded のどちらかの用途か把握してないという文脈はあまりないのではないかという気はする。
なお、デコーダについては、共用するのは正しくない。 headers と application/x-www-form-urlencoded の仕様の違いは厳然として存在するのであり、その違う箇所を使用するエンコーダが存在することを想定しなければならない。そのため、同じ実装を使うのは無理である。
USB に HDD を 2台以上接続しているとデバイス名が毎回変わることも有りうるので、fstab にどう書くか困る。
起動しっぱなしでめったに変わらないからいいか、としていたのだが、ふと、リブートした機会にどうにかできないか調べてみた。
調べてみると、ext2, xfs の場合は fstab に UUID でデバイスを指定できるようだ。やってみると動いた。
Bourne shell のエスケープを調べてみよう。
まず表層的な点からいえば、バックスラッシュ、ダブルクォーテーション、シングルクォーテーションがエスケープに使われる。
では、これらのエスケープ機構が表現しているものはなにかというとそこが難しい。
これは、エスケープが文字列とは言いがたい部分にも影響を及ぼすからである。
たとえば、次の time と \time の違いを見てみよう。
sh-3.1$ time echo a a real 0m0.000s user 0m0.000s sys 0m0.000s sh-3.1$ \time echo a a 0.00user 0.00system 0:00.00elapsed 0%CPU (0avgtext+0avgdata 0maxresident)k 0inputs+0outputs (0major+183minor)pagefaults 0swaps
time の結果が全然違うが、これは前者が sh 組込みの time なのに対し、後者は /usr/bin/time コマンドという違いによる。
では、\ をつけると組込みのものが使えないかというとそんなことはなくて、set はどちらでも動作する。
sh-3.1$ set BASH=/bin/sh ... sh-3.1$ \set BASH=/bin/sh ...
time と set の違いは、 time が予約語で組込みの構文を示すのに対し、set が組込みのコマンドで予約語ではないところにある。
さて、set の話はおいておくとして、 time のことを考えると、\ というエスケープの有無の違いで、組込みの構文と外部コマンド名という意味の違いが現れる。これは、エスケープにより time という単語に複数の表現を持たせ、複数の意味を割り当てているからである。そういう意味では、今までのエスケープと変わらない。
ただ、シェルは、C や Ruby といった言語に比べ、非常に多くの場所にエスケープが現れるというのが違う。 C や Ruby では文字列リテラルなどの、ダブルクォーテーションなどで括られた場所だけでエスケープを使うが、シェルでは time や set の例のように、とくに括られていない場所でも使用される。
考えてみると、C で文字列リテラルの中だけ考えていられたのはその外ではバックスラッシュが使えなかったからであるが、ダブルクォーテーションというデリミタが変数などと文字列を区別していたともみなせる。 foo という変数と "foo" という文字列はどちらも foo であるが、変数と文字列という意味の違いをデリミタによって割り当てているわけである。シェルではその両方に文字列という意味を割り当てることが多々ある、という考え方もできる。
ちなみにエスケープがすべての場所で使えるわけではない。たとえば、for の変数とか in のところでは使えない。
sh-3.1$ for \i in a b c; do echo $i; done sh: `\i': not a valid identifier sh-3.1$ for i \in a b c; do echo $i; done sh: syntax error near unexpected token `\in'
バックスラッシュの直後に改行がきたら、そのバックスラッシュと改行の 2文字は空文字列を意味する。これにより継続行が実現される。
この処理は構文解析に先だって行われる。したがって、以下のように、継続行を使って for の変数名や time を分割しても、予約語として認識される。
sh-3.1$ for i\ j in a b c; do echo $ij; done a b c sh-3.1$ ti\ > me echo a a real 0m0.000s user 0m0.000s sys 0m0.000s
バックスラッシュの直後に改行以外の文字がきたら、それはその文字自身を意味する。
というわけで、バックスラッシュでは、改行以外の任意の文字を表現できる。
なお、C などと違って、非表示文字を表示文字で表現する \xHH などの機能はない。
シェルでシングルクォーテーションで括ったところは、そのままの内容を示す。ここで、そのまま、というのはバックスラッシュも含んでいて、バックスラッシュによるエスケープは使えない。
では、バックスラッシュ倍増による余裕がないのであれば、シングルクォーテーション自体はどう表現するのか、という疑問が出てくるわけであるが、シングルクォーテーション自体はシングルクォーテーションで括った中では表現できない、というのが答である。
というわけで、シングルクォーテーションで括った中では、シングルクォーテーション以外の任意の文字を表現できる。なお、改行もそのままいれられる。
ただ、シェルでは、文字列を隣接して並べられるので、 'abc'\' などと、シングルクォーテーションで括られた abc とバックスラッシュでエスケープされたシングルクォーテーションを並べれば、連結されるので、バックスラッシュやシングルクォーテーション単独では表現できない文字列があっても、たいして困るわけではない。
シェルでダブルクォーテーションで括ったところでは、内部にバックスラッシュによるエスケープが使用でき、かつ、括っていないところよりも意味を持つ文字が減る。
括っていないところでは、リダイレクトのアングルブラケットとか、コマンドの区切りのセミコロンとかアンパサンドとかバーチカルバーとか、いろいろと意味を持つ文字がたくさんあるが、ダブルクォーテーションで括った中では、バックスラッシュ (\), ダラー ($), バッククォート (`) しか特別な意味を持たない。 (もちろん、これに加えてダブルクォーテーション自身も終端という意味を持つ。)
ダラーは変数展開、コマンド置換、式展開に使われる。
バッククォートはコマンド置換に使われる。
ここでコマンド置換がダラーとバッククォートの両方にあるが、これは $(...) と `...` の 2種類の記法があるためである。これらの違いについては後述する。
展開・置換に使われる 3文字、終端のダブルクォーテーション、バックスラッシュの 5文字のそれらの文字自身を表現するにはバックスラッシュでエスケープすれば良い。
つまり、それら 5文字はエスケープして、それ以外の文字はエスケープしないで、ダブルクォーテーションで括れば、任意の文字列が記述できる。
コマンド置換には $(...) と `...` の 2種類がある。
歴史的にいえば、前者が新しく、後者が古い。
コマンド置換というのは、コマンドを起動して、その標準出力を文字列として取り出す機構である。
つまり、$(...) と `...` の ... の部分にはコマンドが記述される。
ここでコマンドというのは実際には、任意のシェルスクリプトである。たとえば、以下のように for 文も使える。
sh-3.1$ echo `for i in a b c; do echo -n $i; done` abc
さて、任意のシェルスクリプトが使えるということは、コマンド置換というのは、任意のシェルスクリプトをシェルスクリプトの中に埋め込む記法ということになる。つまり、終端をいかに検出するかという点が問題になる。
ここで、任意のシェルスクリプトというのは任意の文字列に比べるとずいぶんと制約されている。そのため、シェルスクリプトとして使用していない部分が存在してそれを終端に選ぶことができる。
これを実現したのが $(...) である。これは終端が閉じ括弧であるが、正しい (完結した) シェルスクリプトの終わりに閉じ括弧を追加したものは正しいシェルスクリプトになり得ないことを利用している。 (ここで対にならない括弧は正しいシェルスクリプトには含まれないから、といってしまいたいところだが、 case のところで対になってない括弧を使う構文がある都合上、言い切れないのが歯がゆい)
しかし、バッククォートは厄介である。バッククォートはコマンド置換の開始という意味と終了という意味を両方持っている。バッククォートで括られた中ではその両方の意味である可能性があるため、それを区別する方法が必要になる。
そこで出てくるのがエスケープである。エスケープにより、バッククォートの表現を 2種類に増やし、エスケープされていないほうをコマンド置換の終了、エスケープされているほうをコマンド置換の開始に割り当てる。
あと、これにより、エスケープに使うバックスラッシュ自身もエスケープしなければならない。また、なぜかは知らないが、ダラーもエスケープが可能である。
というわけで、コマンド置換をネストするには以下のようになる。
sh-3.1$ echo `echo \`echo foo\`` foo sh-3.1$ echo $(echo $(echo foo)) foo
なお、バッククォート自身という文字を表現するには、もともとエスケープする必要があったわけで、それをバッククォートによるコマンド置換の内部で使うには、その 2文字をそれぞれエスケープすることになる。
sh-3.1$ echo `echo \\\`` `
というように、エスケープにもいろんな性質のものがある。似ているけど微妙に違うとかいうものもあるので、使い分けるのは簡単ではない。そして、やりそこねると問題が生じる。
そこで、どうやって失敗を避けるか、という点が重要になる。
一番いいのは、エスケープなんて考えなくてもいい、という状況をつくり出すことである。もしそうなれば、失敗することが不可能になる。
たとえば、C言語のプログラムを書いているときには、文字列リテラルをエスケープした形で記述するわけであるが、これをアンエスケープし忘れるということはないし、また、プログラムの中でエスケープ・アンエスケープする必要もない。これは C言語処理系がアンエスケープを自動的に行い、プログラマが記述するプログラムはアンエスケープした文字列だけを扱っていればいいからである。
そのようにするためには、プログラムと外界の間で漏れなく変換を行わなければならない。上記の C言語の場合の外界というのは、テキストで表現されたプログラム自身のことであり、動作しているプログラムがアクセスできるプログラムテキスト内の文字列リテラルはすべてコンパイラを通ってアンエスケープされたものである。その結果として、文字列リテラルはすべてエスケープシーケンスが解決された形でしかアクセスできない。
それと同じことがウェブ関連でも実現できると幸せである。
具体的にどうするかというと、確実な手段もそうでないものもあるが、以下のようなやりかたを思い付く。
・ 入出力とエスケープ・アンエスケープを同時に行う ・ エスケープされているものとされていないもので型を変える ・ 値を見ればエスケープされているかどうかわかるようにする ・ エスケープするときに複数の文字列をまとめる
入力に伴ってアンエスケープを行い、また、出力に伴ってエスケープを行えば、プログラム内部にはエスケープした形の文字列は入り込まない。
そうなれば、プログラムがエスケープ・アンエスケープを明示的に行わない限り、エスケープを間違うというのはありえなくなる。また、明示的に行った途端、それは間違いとなる。
この類のやりかたは、上記の C コンパイラがそうであるが、 CGI で環境変数 QUERY_STRING を application/x-www-form-urlencoded とみなしてデコードするルーチンも該当する。これらは入力においてアンエスケープする例である。
ただ、QUERY_STRING は、application/x-www-form-urlencoded ではないこともあるため、それ以外の解釈が可能であるよう、エスケープ済みのものをプログラム内部に漏らす必要もあるのが残念ではある。これは http URI における query の意味を決めなかった RFC:2616, あるいは URI 全体において query の意味を決めなかった RFC:3986 がその責めを負うべきであるが、いまさらいってもしょうがないという面はある。
出力では、テンプレートエンジンが展開結果を直接外部に送るという形も考えられる。 (HTree.expand_template が文字列を返すのでなく、デフォルトで標準出力に送るのはそれが理由である)
ただ、出力もキャッシュなどでとっておきたいこともあるので、外部に送る以外絶対許さない、というわけにもいかない。
入出力とエスケープを不可分にして不都合が生じないのであればそちらのほうがいいのだが、複雑な木構造を組み上げたり分解したりするには、入出力とエスケープ・アンエスケープを一気に行うというのは難しいことがある。 (単にプログラマがそういうスタイルに慣れていないだけではないのか、という可能性はあるのだが)
その場合、部分木をファーストクラスオブジェクトとして扱い、段階的に入出力データを扱う必要がある。その部分木は、入力したデータを分解した結果かもしれないし、出力するデータを組み上げていく途中かもしれない。
ここで、そのオブジェクトの実体は、エスケープにより木構造がエンコードされた文字列かもしれないし、実際にオブジェクトで木構造が組み上げられているかもしれない。
ここで、文字列にエンコードされている場合にその文字列自体をプログラムに直接扱わせると、エスケープされているものとエスケープされていないものが同じ文字列という型になってしまって、混同しやすい。
そこで、せめて、新しいクラスをつくってラップして、文字列とは違う型にしよう、ということである。ここで中身が文字列であるか木構造であるかは実装を隠蔽すればあまり関係ない (かもしれない)。
そうやって区別できれば、エスケープ済みかどうかを取り違える可能性を減らせる。これは、型によって、どちらであるかを判別できるからである。
このようにするとき、普段使う場合には、そのクラスのオブジェクトの生成がエスケープとなり、そのオブジェクトから情報を取り出すのがアンエスケープとなる。
ここで、文字列とそのクラスの変換であるが、エスケープ・アンエスケープを行う適切な変換は容易になるよう簡単なものを充分に提供するのが望ましい。
しかし、環境変数 QUERY_STRING は、CGI という文脈で見ればエスケープ済みであるが、 Unix という文脈で見れば文字列である。この文字列から件のクラスのオブジェクトを生成するのはエスケープではない。そういう、裏口のようなオブジェクト生成方法も必要になる。
ただ、これはあくまでも裏口であって、使用は勧められない。したがって、そのようなオブジェクト生成はあるていど面倒にしておくべきであろう。メソッド名を長くしておくとか。
また、逆にエスケープ済みの文字列を取り出すのもあるていど面倒にしておくべきかもしれない。でも、それは難しいかも。
あと、いくつクラスを作るのか、という点が問題になる。たとえば、URI では、区切りを意味すると指定できる 18文字があるわけだが、それらにどういう意味をつけたかで違う型にするのかどうか、とか。あるいはシェルではどうするのか、とか。
エスケープの記法毎とか、構文規則の非終端記号毎とか、いくつか可能性は考えられるが、どれが使い易いだろうか。
文字列がエスケープ済みかどうかをとりちがえやすいことが問題なわけだが、両方を文字列で表現するとしても、もし値を見て区別がつくなら混同しにくい。
このような例としては、Ruby の String#dump があげられる。このメソッドは、文字列を受け取り、eval するとそれ自身になるような文字列を返すものであるが、これは文字列を Ruby の文字列の記法にエスケープするとみなせる。
p "abc".dump => "\"abc\""
ここで、エスケープというが、両端のダブルクォーテーションが結果に含まれている。つまり、abc という長さ 3 の文字列をエスケープして "abc" という長さ 5 の文字列が得られている。
ここで、"abc" という文字列だけ見ると、ダブルクォーテーションで括られているため、おそらくエスケープ済みであろうと理解できる。
もちろん、これはエスケープ済みではなく、単に偶然両端にダブルクォーテーションがついている文字列だという可能性もある。しかし、普通にテストしていれば、そういう偶然ではないテストケースも試すであろうから、かなりの割合で判別できることが期待できる。
Web では、HTML の属性値に応用できるのではないだろうか。
ひとつの文字列をエスケープしてひとつのエスケープ済みオブジェクトを生成する手続きは、最終的に生成する木構造のひとつの葉を生成する手続きである。
ここで、ひとつの葉でなく、いくつかの隣接する葉をまとめてエスケープする手続きを用意するのはどうだろうか。
たとえば、application/x-www-form-urlencoded をエンコードするのに、ひとつのコントロール名ないし値をエスケープする手続きを用意するのでなく、コントロール名と値の対の並びを受け取り、 application/x-www-form-urlencoded としてまとめられたひとつのオブジェクトを生成するというわけである。
これは、エスケープという処理を呼び出すにあたって、ひとつの文字列があるというだけでなく、いくつかの関連する情報が揃っていることを要求することになる。そうやって、呼び出せる文脈を限定し、なるべく正しい場所でエスケープを行わせる、ということができるかもしれない。
あと、そうやって複数の文字列をまとめると、その結果には区切り文字が入るので、その結果自体を見てそれがエスケープ済みかどうかがわかるという効果もある。
木構造を作るとして、非破壊的に行うとすれば、葉から組み上げることになる。根ができるのは、いちばん最後である。
しかし、このことは、葉を生成する手続きが、必ずしもその文脈を把握できないということも意味する。
たとえば、「URI に必要なパーセントエンコーディングを行う」というだけの手続きがあったとすると、それが使われる文脈が userinfo であるか、reg-name であるか、segment であるか、query であるか、fragment であるか、あるいは、application/x-www-form-urlencoded のコントロール名や値に使われるのか、という URI のどの部分に利用されるのか、という情報が与えられない。
その情報がなければ、どの文字が区切り文字としての意味を持っていて、パーセントエンコーディングすべきか、ということは完全には判断できない。
これを解決するには、ひとつは、その情報を与える、ということである。たとえば、application/x-www-form-urlencoded の値としてパーセントエンコーディングせよ、と指定するわけである。
また、もうひとつ、エスケープを行うのは木構造を根まで作り上げた後にエスケープするということも考えられる。いったん根までできてからエスケープすれば、文脈に応じたエスケープを自動的に選ぶことが可能になる。
ただ、http URI の query のように、規格ではそこの意味が規定されていない、ということになると、木構造を見ただけでは適切なエスケープを選ぶことはできず、結局そこが application/x-www-form-urlencoded であるというような情報を与えることが必要になる。
エスケープされているかどうかで型を別にする場合、型やクラスをいくつ作るか、という問題が生じる。
関数型プログラミングの教えによれば、文法があれば algebraic data type を使って、非終端記号に対応して型を作り、導出規則に対応してコンストラクタを作る、というのが常套手段である。
それをオブジェクト指向でやれば、非終端記号に対応して抽象クラスを作り、導出規則に対応して具象クラスを作る、となる。導出規則に対応するクラスには、導出規則の右辺の各コンポーネントを取り出すメソッドが用意される。
しかし、ここで問題なのは、規格に載っている文法は変わることである。 URI でいえば、 RFC 1738, 2396, 2732, 3986 と更新されるたびに文法はずいぶんと変わっている。非終端記号の名前も変わっており、そういう不安定な情報に基づいて型を決めれば、将来の規格の更新によってコードが古びることはほとんど決まったようなものである。
また、オブジェクトを生成する時点でクラスを決定できるか、という問題もある。たとえば、環境変数 QUERY_STRING をオブジェクトにしようとした場合、 CGI の規格からはそれが URI の query であることはわかるが、 application/x-www-form-urlencoded かどうかはわからない。つまり、アプリケーションがそのことを教えてやるまでオブジェクトの生成を遅延しなければならない。
クラスの種別にたくさん情報を載せると、このような問題が発生する。
逆に、クラスの種別に載せる情報を最小限にして、エスケープされているかどうかしか載せないとすると、文字列ではないクラスがひとつあれば足りることになる。この場合、パーセントエンコーディングであろうが文字参照であろうがシェルであろうが、ひとつのクラスで表現される。
もちろん、こうするとエスケープのやりかたや文法との対応がクラスで表現されないので、それらを混同することが可能になってしまう。
クラスの種別で表現する情報をどの程度にするかはいくつかのやりかたが考えられる。
ここで、入力が低位レイヤから文字列として渡され、それをなんらかの情報によりエスケープされた文字列を表現するオブジェクトとしてラップし、そのメソッドによりコンポーネントを取り出す、ということを考える。そのような処理は、結局、オブジェクトを生成してそのメソッドを呼び出すことになるので、 Foo.new(str).bar というコードになる。したがって、Foo と bar にどのような情報を含ませるか、というのが問題となるが、いくつかの可能性が考えられる。
・ ApplicationXWWWFormURLEncoded.new(ENV['QUERY_STRING']).decode ・ URIQuery.new(ENV['QUERY_STRING']).application_x_www_form_urlencoded ・ PercentEncoded.new(ENV['QUERY_STRING']).application_x_www_form_urlencoded ・ Escaped.new(ENV['QUERY_STRING']).application_x_www_form_urlencoded
ここでは環境変数 QUERY_STRING からオブジェクトを生成して application/x-www-form-urlencoded としてデコードしている。これを行うには、環境変数 QUERY_STRING が URI の query 部であり、それはパーセントエンコーディングされていて、 application/x-www-form-urlencoded という形式になっているはず、という情報を与えなければならない。これらの情報のどこまでをクラスの種別で表現するか、という点が上記4つのそれぞれの違いである。
ApplicationXWWWFormURLEncoded は、前述のとおり、 application/x-www-form-urlencoded であることがわかるまで生成できないという問題がある。
URIQuery は、導出規則に対応するクラスなので、RFC の変化に弱そうである。まぁ、query だけであれば安定しているような気がするが、 query にはクラスを作って他の導出規則には作らないとすれば、どこまで作るのかという問題が生じる。
PercentEncoded は、 query ではないものにも使用されることになるので、 application_x_www_form_urlencoded というメソッドが URI の query 以外の部分にまちがえて適用してしまう可能性が出てくる。
Escaped は、 URI でさえないものにも使用されるので、 application_x_www_form_urlencoded というメソッドが間違えて適用できる可能性がひろがる。また、メソッド名は URI 以外の用語と衝突する可能性があるため、 uri_application_x_www_form_urlencoded とプリフィクスを付けるほうがいいかもしれない。まぁ、application/x-www-form-urlencoded という名前だけについていえば長いのであまり衝突しそうにないが。
デコードは入力からデータを分解していく処理であったが、逆に出力を構成するためにデータを組み上げていく場合の処理は、オブジェクトの生成となる。ここで 2つのデータを組み上げてひとつのオブジェクトを生成する場合を想定すると、以下の可能性が考えられる。
・ foo(data1, data2) ・ Foo.new(bar).baz(data1, data2) ・ data1.foo(data2)
最初のは関数である。この場合、名前は foo というひとつしかないので、その名前でエスケープの種別を表現するという以上の選択肢はない。なお、クラスメソッドも、クラス名とメソッド名を直接的に記述する形式で使う限り、同様である。
次のは、ファクトリーパターンである。 Foo, bar, baz でエスケープの種別を表現するわけであるが、アプリケーション毎に定義しなければならないほどエスケープというのは多種多様なのか、という点が疑問である。規格で定義されるのであれば、個々の規格に対応するものをライブラリで提供すればいいのではないだろうか。ただ、ライブラリの内部でこういう形の実装を利用するというのはありうるかもしれない。
最後のは、下位のコンポーネントのメソッドで組合せを行うというものである。エスケープの種別はそのメソッド名で指定する。 data1 が文字列の可能性がある場合、文字列にメソッドを追加する必要が出てくる。
なお、このようにオブジェクトを生成する場合、どの生成方法でもエスケープの意味はその時点ではっきりしている。たとえば、application/x-www-form-urlencoded なものを生成するという意図があれば、生成されたオブジェクトは application/x-www-form-urlencoded に従う。とすると、ApplicationXWWWFormURLEncoded というクラスを使えばその情報をクラスの種別に載せられる。しかし、URIQuery, PercentEncoded, Escaped であれば、その情報は一部抜け落ちる。
ただ、抜け落ちる情報を捨てるのでなく、インスタンスに記録することはありうるかも。
どんなクラスを選ぶかはいくつかの選択肢がある。
結局、長期的に変わらないこと、それでいてプリフィクスを付けなくてもメソッド名が衝突しない程度には分かれていること、あたりが狙い目だろうか。
エスケープの場合には、エスケープの種別 (パーセントエンコーディング、文字参照、シェル) くらいには分かれていてもよくて、場合によっては文法をちょっと加味してさらに分けてもいい、というところだろうか。
ただ、そのくらいの分けかただと、厳密には混同を防ぐことはできない。たとえば、userinfo な PercentEncoded を application/x-www-form-urlencoded と思ってデコードしてしまうことができる。
API のデザインは、やってほしいことをやりやすくして、やってほしくないことをやりにくくするわけだが、これはアメとムチである。
上記の、userinfo な PercentEncoded を application/x-www-form-urlencoded と思ってデコードしてしまうことを禁止できない、というのは、ムチの部分の実現が完全でない、ということである。これが完全に実現されるかというのは、使いやすさにとの関係は間接的である。もし、人間がその間違いをよく起こすのであれば、間違いを防止してくれるという意味で使い易さの向上になるが、あまり起きないのであれば、禁止できたところでたいして変わらない。
一般に、禁止するというムチの部分は、人間が間違いやすいのであれば意味があるが、もともと間違えないのであれば使いやすさはあまり変わらない。
それに対して、アメというのは、実際に使用するところを使いやすくする部分である。たとえば、application/x-www-form-urlencoded のエンコードで、
f([[k1, v1], [k2, v2]])
というように、複数の文字列を受け取ってまとめるようなものは、
"#{f(k1)}=#{f(v1)}&#{f(k2)}=#{f(v2)}"
というようにひとつひとつをエスケープするものよりも短くて使いやすい。これが使いやすくなっているのは、ひとつの文字列のエスケープに対し、ほぼ確実にその周囲で行われる処理があり、それを同時にやってくれるためである。
一般に、高確率で連続する処理をひとつにまとめるというのは、イディオムのメソッド化であり、使いやすさに貢献する。
考えてみると、型を変えるというのはムチの側面が強い。もし、人間が絶対失敗しないなら、すべて文字列で済ませば、型変換を明示的に記述しなくていいぶんコードを減らせる。
前に 4つあげたエスケープ・アンエスケープを間違えにくくするやりかたについて、アメとムチに分類するとだいたい以下のようになる。
・ [アメ] 入出力とエスケープ・アンエスケープを同時に行う ・ [ムチ] エスケープされているものとされていないもので型を変える ・ [アメ] 値を見ればエスケープされているかどうかわかるようにする ・ [アメ] エスケープするときに複数の文字列をまとめる
型を変えるというのは前述のとおりムチである。ただ、クラスで分類すると短いメソッド名を使えることがあるので、何回もメソッドを呼び出すのであればその部分はアメかもしれない。
複数の文字列をまとめるのは前述のとおりアメである。
入出力と同時に行うというのは、入出力という処理とエスケープ・アンエスケープを連続して行うというイディオムをメソッドかしたものとみなせて、これはアメである。
値でエスケープ済みかどうかわかるようにするのは、デリミタをつけておくという話で、デリミタをつけるという処理をまとめてイディオムということで、これもアメである。
型を変えると、レイヤ間でギャップが生じる。上位レイヤでオブジェクトなものが、下位レイヤでは文字列になることがある。
これらの間の型変換は、上位レイヤにおいてその文字列がそのオブジェクトであるという情報を提供することである。ここで、エスケープ・アンエスケープ関数が文字列に対して動作するのであれば、その関数名がそれと同じ情報に加えてエスケープ・アンエスケープの形式の情報を提供する。
したがって、型変換の上でエスケープ・アンエスケープメソッドを呼び出すのは、結局のところ同じ情報を提供しなければならない上、 2回にわけて提供するぶん面倒臭い。
しかも、これはやってほしくないことではなくてやってほしいことであるから、やってほしいことが面倒臭くなっているというよろしくない状況である。
この部分をどうにかできるか、と考えると、ぜんぶ文字列でやった上で、エスケープ・アンエスケープの呼び出しが一貫していることを検査するというのが考えられなくはないか。
型を分けたとすれば Foo と Bar というクラスおよび Foo#baz と Bar#qux というメソッドがあるとして、型を String だけにしておいて String#baz と String#qux を用意し、ひとつの String インスタンスに対して baz と qux を両方呼んだら例外。うぅむ。意外過ぎるか。
[latest]