ものがたり(旧)

atsushieno.hatenablog.com に続く

invalid characters make System.Xml suck

Saxon.Apiもなかなか興味深い。Michael Kayによると、もともとはSystem.Xml.Xslと同じAPIにしようと思っていたけど、インターフェースというより単なる実装で、しかもLoad()やTransform()のオーバーロードが鬱陶しい、明らかにAPIリファクタリングが必要だ、ということでやめたらしい。

ところで、XmlTextReaderをそのままXMLAPIの入力に使っている人は、結構多いんじゃなかろうか。それは危険なのでやめておいた方が良い。Saxon.Apiのドキュメントには、各所にUse of a plain XmlTextReader is discouragedと書いてある。


... because it does not expand entity references. This should only be used if you know in advance that the document will contain no entity references (or perhaps if your query or stylesheet is not interested in the content of text and attribute nodes). Instead, use an XmlValidatingReader (with ValidationType set to None).
これはDocumentBuilder.Build(XmlReader)の説明に書いてある。

XmlTextReaderって、けっこう中途半端なんだよねえ。テキスト文字列を読み込む場合のパフォーマンスとか。

XmlReaderにべったりな連中はよく「SAXは常にcharacters()でStringを生成して返すからパフォーマンスが悪い」と言う。だけど、XmlTextReader.Read()って、そんなに優れた設計になっているだろうか?

XmlTextReaderは、Textノードがあったら、その中にchar生成規則から外れる文字があったら、そのノードを読んだとされるRead()呼び出しの時にすぐさまエラーを発生させている。これって、Valueプロパティの文字列を生成するために、そのテキストノードに含まれる全てのUnicode文字を、Valueにこそしなくともchar[]なりStringBuilderなり何なりに保持しているっていうことじゃないか?

次のノードがテキストノードだと判明したら、その時点で読むのを止めておいて、値が要求された時に初めて続きをバッファに全て格納する、要求されなければ次のRead()の時にテキストを読み捨てる。そうすれば、1000000文字のBase64ノードがあっても、内部バッファを1000000文字まで爆発させずに済むんじゃないか、と思うのだ。

そんな設計にしたら、Read()のタイミングとエラー報告のタイミングが合わなくなって混乱するじゃん、と思うかもしれないが、そんな混乱は既に起きているのである。XmlValidatingReaderのschema validityについて考えてみよう。

たとえばsimple typeのenumeration facetか何かで、文字列"ABC"のみをvalidとする型があるときに、

  • NodeType = XmlNodeType.Text, Value = "AB"
  • NodeType = XmlNodeType.Text, Value = "C"

というノードが連続してあった場合*1、この一連のノードは"AB"だけでinvalidとなってはならず、"AB""C"全体でvalidでなくてはならない。逆に、"ABC"というテキストノードがあっても、その後に Value = "D"なTextノードが来たらアウトだ。これは、EntityReferenceをResolveEntity()で展開していても起こりうる。

だから、schema validityはEndElementの時(あるいはIsEmptyElement=trueならばElementの時)に判断しなければならない。これはRead()のタイミングと一致していない。

(それに、XmlTextReaderと中途半端に結合しているXmlValidatingReaderが内部的にどうやっているかは知らないけど、Valueプロパティはvalidationを実行するには取得が必須なんだから、Value stringオブジェクトを生成しないことなんて、実際にどれだけあるの?っていう話もある。)

あとついでに書いておくと、XmlTextWriterも中途半端だ。XmlTextWriterって、WriteString(string)で渡された引数にXMLとして使えない文字があると、文字参照つまり&#xX;として出力するのだけど、文字参照になったところでinvalidな文字はinvalidだ。まともなXML処理系は、そんなXMLモドキを読んではくれない。NormalizationプロパティをtrueにしていないXmlTextReaderが何も言わずに読み込んでいるから、ほとんど誰も気付いていないだけだ。

この一見意味がありそうで実は無意味なエスケープ処理を行うために、XmlTextWriterでは、テキストの文字がinvalidなものかどうか、いちいちチェックしているということになる。あほらしすぎる。この無駄な処理を入れたせいで、最近イチから書き直して20%高速化した僕のXmlTextWriterは、それだけで10%もパフォーマンスダウンしてしまった。

そんなわけで、少しでも内容に予測のつかないバイナリデータをXML化するときは、XmlConvertを使って変換してやったり、Base64化して書き込んでやったりしなければならなくなる。それって何かXMLっぽくないし。

そんなわけで、このSaxon.Apiがその辺どうしているのか、興味深いところだ。(まあ、ていうか、単にまともに処理しているだけだろうけど…)

*1:こういう状況が想像できない人は、一方をCDATAセクションだと考えてみると分かりやすいだろう