この記事はkdnaktの1人 Advent Calendar 2020の18日目の記事です(1日遅れ)。
2020年は会社でKotlin dojoを主催して週1回30分Kotlinと戯れていました。12月はその集大成ということで、Kotlin/NativeでHTTPサーバーを作ってみたいと思います。どこまでできるか、お楽しみ……。
[日本語のパスを扱う]
RFC2718では、URLのパスにUTF-8を用いてパーセントエンコーディングすることで日本語などの扱えます。
たとえば、Wikipediaの「ディレクトリトラバーサル」のページのURLをパーセントエンコーディングした結果は以下のようになります。
Kotlin/Nativeでパーセントエンコーディングを扱うにはどうすれば良いのかわからなかったので、Google先生に聞いてみると、KotlinでJavaのURLDecoderを使う方法ばかりが検索結果に表示されました。
Kotlin/NativeではJavaの標準ライブラリを利用できないので、他の方法を探します。Kotlin/NativeではC言語で実装されている関数がある程度使えるので、C言語でのパーセントエンコーディングされた文字列をデコードする方法を調べてみましたが、以下のサイトのようにゴリゴリ実装する方法しかなさそうです。
言語的にはJavaの実装の方が参考にしやすそうだったので、java.net.URLDecoder#decode()関数を参考に、パーセントエンコードされた文字列をデコードする関数を実装します。
java.net.URLDecoder#decode()関数のロジックの概要は以下の通りです。
- エンコードされた文字列を1文字ずつループで処理する
- 処理対象の1文字が
+、%、それ以外で処理を分ける +の場合には、半角スペースをデコード結果文字列に追加する%の場合には、以下の処理を行う%に2文字続いている場合は、続く2文字を16進数とみなして10進数に変換- 変換した数字をByte型に変換して配列に詰める
- 後続の文字列を読込み、%がなくなるまで処理を繰り返す
- 処理が終わったら配列につめたByteを文字列に変換して、デコード結果文字列に追加する
+、%以外の文字の場合は、その文字をそのままデコード結果文字列に追加する
これをKotlinで実装したところ、以下のようになりました。
import kotlinx.cinterop.toKString
fun decodeURL(url: String): String {
val res = StringBuilder()
var i = 0
while (i < url.length) {
when (val c = url[i]) {
'+' -> {
res.append(' ')
i++
}
'%' -> {
var bArr = byteArrayOf()
var curr = c
while (i + 2 < url.length && '%' == curr) {
val hex = url.substring(i + 1, i + 3)
bArr += hex.toInt(16).toByte()
i += 3
if (i < url.length) {
curr = url[i]
}
}
res.append(bArr.toKString())
}
else -> {
res.append(c)
i++
}
}
}
return res.toString()
}
E3やA2といった16進数の文字列をByte型に変換する部分がやや難しかったです。
最初は"E3".toByte()などと実装してkotlin.NumberFormatExceptionを発生させたり、自前でIntに計算しなおした上でtoByte()を呼び出したりしていました。最終的にはString.toInt(radix: Int)を組み合わせることで解決できました。
動作確認のためのテストとして、以下のようなケースをいくつか追加して実行しましたが、問題なさそうです。
@Test
fun testJapaneseKatakana() {
assertEquals("ア", decodeURL("%E3%82%A2"))
}
@Test
fun testSlash() {
assertEquals("/", decodeURL("%2f"))
}
@Test
fun testAlphabet() {
assertEquals("ABC", decodeURL("ABC"))
}
[HTTPサーバーに組み込む]
実装したdecodeURL()関数をHTTPサーバーのコードに組み込み、実際に利用してみます。
これまで、FileReaderクラスに渡されたpathを直接利用していましたが、一度デコードするように修正します。
// 修正前
class FileReader(val path: String) {
fun content(): String {
//
}
}
// 修正後
class FileReader(_path: String) {
val path = decodeURL(_path)
fun content(): String {
//
}
}
publicディレクトリにメモ.txtを配置してサーバーを起動し、ブラウザでhttp://localhost:8080/メモ.txtにアクセスすると、無事ファイルの中身が表示されました。
