goでxmlをescapeする話

はてなブログのクライアントを書いている時にxmlをPOSTする必要があった。考えるのが面倒だったので text/template を使っていたのだけれど。 これでは問題があった。本文がescapeされないのでinvalidなrequestになってしまう。

具体的にはこういう話。

package main

import (
    "log"
    "os"
    "strings"
    "text/template"
)

func main() {
    tmpl := template.Must(template.New("template").Parse(strings.TrimSpace(`
<?xml version="1.0" encoding="utf-8"?>
<content>{{.}}</content>
`)))

    content := `&foo とか&が書かれた文章`
    if err := tmpl.Execute(os.Stdout, content); err != nil {
        log.Fatal(err)
    }
}

これの実行結果が以下の様になってしまう。 & はescapeしなければダメ。

<?xml version="1.0" encoding="utf-8"?>
<content>&foo とか&が書かれた文章</content>

html.templateなら問題なし?

それじゃ自動でescapeしてくれるhtml.templateを使えば良いかというとそうでもない。先頭の<?xml 部分もescapeしようとしてしまう。

package main

import (
    "html/template"
    "log"
    "os"
    "strings"
)

func main() {
    tmpl := template.Must(template.New("template").Parse(strings.TrimSpace(`
<?xml version="1.0" encoding="utf-8"?>
<content>{{.}}</content>
`)))

    content := `&foo とか&が書かれた文章`
    if err := tmpl.Execute(os.Stdout, content); err != nil {
        log.Fatal(err)
    }
}

これはinvalidな出力。

&lt;?xml version="1.0" encoding="utf-8"?>
<content>&amp;foo とか&amp;が書かれた文章</content>

実際

実際同じようなことをやってしまった人がいてissueは却下されている。

https://github.com/golang/go/issues/3133

xmlはhtmlじゃないでしょ。text/templateを使ってescapeしましょうみたいな話。

まじめにescapeしましょう

package main

import (
    "html"
    "log"
    "os"
    "strings"
    "text/template"
)

func main() {
    tmpl := template.Must(template.New("template").Parse(strings.TrimSpace(`
<?xml version="1.0" encoding="utf-8"?>
<content>{{.}}</content>
`)))

    content := `&foo とか&が書かれた文章`
    if err := tmpl.Execute(os.Stdout, html.EscapeString(content)); err != nil {
        log.Fatal(err)
    }
}

今度は大丈夫。

<?xml version="1.0" encoding="utf-8"?>
<content>&amp;foo とか&amp;が書かれた文章</content>

ところで

ところでstruct中のstringを再帰的に辿ってescapeしていく処理が欲しくなったりするんですが。やっぱりreflectパッケージが必要になるしオススメしないみたいな感じになるんですかね。

text/template にはpipelineという仕組みがあるっぽいのでこれを上手く使って全部手動でescapeするのが無難?

もしくは真面目にこちらを使いましょうとか。