この記事は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
にアクセスすると、無事ファイルの中身が表示されました。