本記事の内容は,.Net Framework 4.6で動作確認をしています.
結論
不透明URIに対しては,System.GenericUriParser
を基底クラスにするのではなく,System.UriParser
を基底クラスにするのがよい.
new Uri("data:,%30")
の結果がおかしい
まず最初に書いておかないといけないが,System.Uri
はdata URIスキームをサポートしていない.
ただ,サポートしていないURIスキームであっても,何となく処理してくれる.
var uri = new Uri("data:,0");
Console.WriteLine(uri);
// stdout> data:,0
ところが,パーセントエンコードするとおかしくなる.
// 以下のコードには,パーセントエンコードを実行したときの文字エンコーディングが何だったのかという問題があるが,
// ひとまず無視しておく
// (ちなみに,C#はUTF-8だと思ってパーセントエンコードをデコードするようだ)
var uri = new Uri("data:,%30");
Console.WriteLine(uri);
// stdout> data:%30,0
単なるバグだと思うが,どういう理屈でこのような結果になるのか理解に苦しむ..
やはり非サポートURIスキームなのに,無理やりSystem.Uri
に食わせたのがよくない.仕方ないのでちゃんと実装しよう..
どうやって実装するんだ?
System.UriParser.Register()
を使う.
https://msdn.microsoft.com/ja-jp/library/system.uriparser.register%28v=vs.110%29.aspx
これらをキーワードにGoogle検索すると,少ないが情報が見つかる.
- http://www.ageektrapped.com/blog/writing-your-own-uriparser
- http://www.ageektrapped.com/blog/writing-your-own-uriparser-part-2-implementing-getcomponents/
- http://www.codeproject.com/Articles/13773/Writing-a-custom-UriParser-for-NET
1と2は続き物で,3がその集大成と言ったところか.どちらの場合もSystem.GenericUriParser
を基底クラスとするサンプルが書かれている.
しかし,data URIのような不透明URIを追加する場合,System.GenericUriParser
を基底クラスにするのは適切ではない.なぜならば,本家のAPIドキュメントにこう書かれているからだ.
https://msdn.microsoft.com/ja-jp/library/system.genericuriparser%28v=vs.110%29.aspx
A customizable parser for a hierarchical URI
System.GenericUriParser
は階層URIに対するものだと明確に書かれている.実際,以下のようなコードを書いて
class DataUriParser : GenericUriParser
{
public DataUriParser() : base(..省略..)
{
System.Diagnostics.Debugger.Break();
}
}
this
の非公開メンバー変数m_Flags
を確認すれば,APIドキュメントに書かれてあることが正しいとわかる.
MustHaveAuthority | ..
ちなみに,m_Flags
の値はGenericUriParserOptions.AllowEmptyAuthority
などを渡しても変化しない.
このような設定になっている結果,不透明URIに対してSystem.GenericUriParser
を基底クラスにするといろいろと面倒なことになる.
Stack OverflowにSystem.Reflection
を使ってm_Flags
を強引に書き換えるという荒業も紹介されているが,正直どうかと思う.
素直にSystem.UriParser
を基底クラスにしよう
実際にやってみたところ,m_Flags
はMayHavePath
となっており,不透明URIにはちょうどよいことが分かった.
System.UriParser
は抽象クラスなので,必ず発生クラスを作る必要があり,実装するURIスキームの仕様に合わせてSystem.UriParser.InitializeAndValidate()
をオーバーライドするのがよいだろう.
class DataUriParser : UriParser
{
protected override void InitializeAndValidate(Uri uri, out UriFormatException parsingError)
{
// uri.OriginalStringなどを再度解析して,不正な形式ならparsingErrorを設定
}
}
余談
System.UriParser
の設計は,かなり難解だ.正直,ちゃんと理解できたという自信はない.
最初,System.GenericUriParser
を基底クラスにしていろいろ試していたが,
-
System.GenericUriParser.InitializeAndValidate()
を必ずオーバーライドしないと例外発生 -
System.GenericUriParser.InitializeAndValidate()
をオーバーライドした場合は,System.GenericUriParser.GetComponents()
も必ずオーバーライドが必要っぽい
どうやらnew Uri("...")
しただけでは文字列解析は十分に行われることはなく,System.Uri.Query
などURIの各パーツへのアクセスが発生しないと派生クラスは機能しないようだ.アクセスが発生すると上記のメソッドが呼び出される.
System.GenericUriParser.GetComponents()
の呼び出しのたびに文字列を解析するのは非効率なので,そう考えるとクラスのメンバー変数に解析結果を保持するのが妥当だが,そうするならSystem.GenericUriParser.OnNewUri()
もオーバーライドが必要だろう.
あと,System.GenericUriParser
のコンストラクターにはSystem.GenericUriParserOptions
を渡すのだが,System.GenericUriParser.GetComponents()
をオーバーライドしている場合,コンストラクターに指定したフラグ値を考慮した実装を行う必要があるようだ.
例えば,コンストラクターにSystem.GenericUriParser.NoQuery
を含めた場合,System.GenericUriParser.GetComponents()
は"scheme://authority/path?query=value"を"scheme://authority/path%3Fquery=value"と変換する.しかし,System.GenericUriParser.GetComponents()
をオーバーライドすることで,このフラグ値やメソッド引数の指示を無視して任意の文字列を返すことが可能だ.このとき,例外などは一切発生しない.