非存在は人類には早すぎた

どうもこんにちは。 皆さん、存在してますか?

一般に人間は非存在が苦手です。 null, empty, absent などなど、非存在が人類には早すぎた例は枚挙に暇がございません。 その中から今回紹介するのはこちら、 RFC 3986 Uniform Resource Identifier (URI): Generic Syntax (2005年産) です。 インターネットネイティブな非存在のテイストをお楽しみください。

この記事は IQ1 Advent Calendar 2019 の19日目の記事です。

TL;DR クイズ

問題です。 解くのに必要な IQ は 1 です。

RFC 3986 では URI 文字列の文法などが規定されていますが、その中で「authority (ホスト名など) がないとき、パスが // から始まってはいけない」という制約が提示されています。 では、 file:////etc/passwd という文字列は、 RFC 3986 で規定された URI の文法に適合しているでしょうか。 それとも、適合していないでしょうか。

追加のクイズ: 以下の文字列が、妥当な URI か不正な URI か判別してください。

  • foo:
  • foo:/
  • foo://
  • foo:///
  • foo:////
  • foo://///

答えは 結論 セクションをご覧ください。

URI とは何ぞや

URI (Uniform Resource Identifier) とは、 web でお馴染みの URL をちょっと拡張した感じの概念です。 URL (Uniform Resource Locator) は名前の通り「場所」を指すために使われるものでした。 これを単に場所として使うだけでなく、 web 上のあらゆるリソースを一意な名前で指すための識別子として使えると、データの共有や連携が捗るよねという寸法です。 たとえば RFC 3986 では以下のような例が挙げられています。

  • ftp://ftp.is.co.za/rfc/rfc1808.txt
  • http://www.ietf.org/rfc/rfc2396.txt
  • ldap://[2001:db8::7]/c=GB?objectClass?one
  • mailto:John.Doe@example.com
  • news:comp.infosystems.www.servers.unix
  • tel:+1-816-555-1212
  • telnet://192.0.2.16:80/
  • urn:oasis:names:specification:docbook:dtd:xml:4.1.2

こういった (そしてこれらに限らない) さまざまなリソースを共通の統一的な文法で指すことができるようになるのが URI という概念の偉大さです。

URI の文法

URI の文法は、主に RFC 3986 の §3 (Syntax Components) で、 ABNF を用いて定義されています。

        URI         = scheme ":" hier-part [ "?" query ] [ "#" fragment ]

hier-part   = "//" authority path-abempty
            / path-absolute
            / path-rootless
            / path-empty

scheme      = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )

authority   = [ userinfo "@" ] host [ ":" port ]

path          = path-abempty    ; begins with "/" or is empty
              / path-absolute   ; begins with "/" but not "//"
              / path-noscheme   ; begins with a non-colon segment
              / path-rootless   ; begins with a segment
              / path-empty      ; zero characters

path-abempty  = *( "/" segment )
path-absolute = "/" [ segment-nz *( "/" segment ) ]
path-noscheme = segment-nz-nc *( "/" segment )
path-rootless = segment-nz *( "/" segment )
path-empty    = 0<pchar>
      
URI の文法定義の一部抜粋

こんな感じです。 読めますね。

制限

しかし残念ながら話はこれで終わりません。 §3 では、以下のような但し書きが存在します。

The scheme and path components are required, though the path may be empty (no characters). When authority is present, the path must either be empty or begin with a slash ("/") character. When authority is not present, the path cannot begin with two slash characters ("//"). These restrictions result in five different ABNF rules for a path (Section 3.3), only one of which will match any given URI reference.

RFC 3986, §3 (Syntax Components), , 強調は引用者による

ややこしくなってきました。 一応日本語にしておくと、「authority がある (present) なら、 path は空であるか "/" で始まる。 authority がない (not present) なら、 path は "//" で始まってはいけない」と書いてあります。

ところで、 authority の定義を見てみると、 authority は空になることができます。

          URI         = scheme ":" hier-part [ "?" query ] [ "#" fragment ]

hier-part   = "//" authority path-abempty
            / path-absolute
            / path-rootless
            / path-empty

scheme      = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )

authority   = [ userinfo "@" ] host [ ":" port ]
host        = IP-literal / IPv4address / reg-name
reg-name    = *( unreserved / pct-encoded / sub-delims )
        
関連部分再掲

たとえば file:////etc/passwd という文字列を考えてみると、 file が scheme なのは良いとして、 ////etc/passwd は一体なんなのでしょうか。 <scheme="file">://<authority=""><path="//etc/passwd"> のようにパースされるべきでしょうか? それとも <scheme="file">:<path="////etc/passwd"> のようにパースされるべきでしょうか? いずれにせよ、「authority がないなら path は // で始まってはいけない」に違犯している気がします。 でも、本当に? 空の authority は absent だと見做すべきなのでしょうか? もしそうでないなら前者は valid ということになります。

file:////etc/passwd が URI として invalid なのかそうでないのか。 この記事では、その謎に迫ります。

手掛かり

もうひとつの表現

実は「"//" で始めては駄目」を別の場所で別の表現を使って書いている部分があり、読解のための手掛かりになります。

If a URI contains an authority component, then the path component must either be empty or begin with a slash ("/") character. If a URI does not contain an authority component, then the path cannot begin with two slash characters ("//").

RFC 3986, §3.3 (Path), , 強調は引用者による

今回は When authority is present よりも表現が多少明確になっています。 authority が present か absent かではなく、 authority component を持っているか持っていないかで条件を指定していますね。 ここでは「空文字列は present なのか absent なのか」ではなく「component を持つかどうか」を考えれば良さそうなので、空文字列へのマッチも component の存在と見做せる気がしてきます。 つまり <scheme="file">://<authority=""><path="//etc/passwd"> は「authority component を持っている」といえそうです。

制限とマッチの曖昧性

もうひとつ気になるのが、 present なら〜 の文の直後に書かれたこの文です。

These restrictions result in five different ABNF rules for a path (Section 3.3), only one of which will match any given URI reference.

RFC 3986, §3 (Syntax Components),

この記述を踏まえると、もしかしてこの制約は ABNF の文法に対して追加の制約を課すものではなく、文法のパースの曖昧性をなくすための制約である可能性が読み取れます。 もしそうなら、 <scheme="file">://<authority=""><path="//etc/passwd"> の形でパース可能な file:////etc/passwd は妥当な URI ということになるでしょう。

ABNF のコメント

Errata にも、若干気になる記述があります。

In fact, the ABNF here is more specific than it often is. In other RFCs it will say things like
xyz = 0 / %x31-39 *2DIGIT ; valid values are 0-255
...and just let the comment restrict the maximum value.

Verifier notes for Errata ID 4393, , フォント変更は引用者による
RFC 3986 で利用されている ABNF (RFC 2234) [0]についての言及

この verifier note によれば、 RFC で利用される ABNF は構文解析アルゴリズムを直接に提示するものではなく、必要とあらばコメントを与えることで formal な文法として読み取れる以上の制約を与えるといった行為が行われるようです[1]。 となると、文法定義中に直接記述されていない // に関する記述は、文法をより強く制限するものではなく曖昧さをなくすだけのもので、文法が受理する文字列の集合自体に影響を与えないと考える方が自然のように思われます。

結論

ここまでで挙げた手掛かりから、 // に関する制約は ABNF が受理する文字列を少なくしないと考えるのが自然であるといえます。

たとえば foo:// が ABNF だけを見ると <scheme="foo">://<authority=""><path=""><scheme="foo">:<path="//"> のどちらとしても解釈できるわけです。 ここで「authority がないなら path は // で始まってはいけない」のルールが曖昧性排除のためのルールだと考えると、後者の解釈が排除され、前者の解釈のみが可能になります。 これにより一意な構文解析が可能になりました。

クイズの答え

さて、この理解を前提に RFC 3986 を読むと、記事冒頭で出したクイズの答えは「全部妥当な URI として解釈できる」です。

  • file:////etc/passwd<scheme="file">://<path="//etc/passwd">
  • foo:<scheme="foo">:<path="">
  • foo:/<scheme="foo">:<path="/">
  • foo://<scheme="foo">://<authority=""><path="">
  • foo:///<scheme="foo">://<authority=""><path="/">
  • foo:////<scheme="foo">://<authority=""><path="//">
  • foo://///<scheme="foo">://<authority=""><path="///">

結局何が厄介だったのか

結局のところ何が規格読解を困難にしていたかというと、 When authority is not presentIf a URI does not contain an authority component という文面が、空文字列を "not present" や "does not contain" と見做す文章なのかが曖昧だったというところにあります。 まるで RDB の空文字列と NULL を同一視するかどうか問題みたいですね。

IQ1 には、空文字列と存在しない文字列を区別するのはあまりに大変でした。 皆さんが規格を書くようなことがあれば、空のものと非存在を明確に区別できるよう表現を工夫したり付記を残すことをおすすめいたします。

おまけ

何でこんなこと考えたの

妥当な URI (RFC 3986) / IRI (RFC 3987) の文字列のみを持てるような型を実装したかったからです。 成果は iri-string crate として公開しています。 ドキュメントにも同様の説明を追加 しました。 もし URI / IRI を用いる Rust プログラムを書く機会があればどうぞ。

参考文献