Kotlin/NativeでHTTPサーバーを作るアドベントカレンダー(18日目:パーセントエンコーディング)

この記事はkdnaktの1人 Advent Calendar 2020の18日目の記事です(1日遅れ)。

 

2020年は会社でKotlin dojoを主催して週1回30分Kotlinと戯れていました。12月はその集大成ということで、Kotlin/NativeでHTTPサーバーを作ってみたいと思います。どこまでできるか、お楽しみ……。

 

 

[日本語のパスを扱う]

RFC2718では、URLのパスにUTF-8を用いてパーセントエンコーディングすることで日本語などの扱えます。

たとえば、Wikipediaの「ディレクトリトラバーサル」のページのURLをパーセントエンコーディングした結果は以下のようになります。

https://ja.wikipedia.org/wiki/%E3%83%87%E3%82%A3%E3%83%AC%E3%82%AF%E3%83%88%E3%83%AA%E3%83%88%E3%83%A9%E3%83%90%E3%83%BC%E3%82%B5%E3%83%AB

 

Kotlin/Nativeでパーセントエンコーディングを扱うにはどうすれば良いのかわからなかったので、Google先生に聞いてみると、KotlinでJavaのURLDecoderを使う方法ばかりが検索結果に表示されました。

Kotlin/NativeではJavaの標準ライブラリを利用できないので、他の方法を探します。Kotlin/NativeではC言語で実装されている関数がある程度使えるので、C言語でのパーセントエンコーディングされた文字列をデコードする方法を調べてみましたが、以下のサイトのようにゴリゴリ実装する方法しかなさそうです。

rosettacode.org

 

言語的にはJavaの実装の方が参考にしやすそうだったので、java.net.URLDecoder#decode()関数を参考に、パーセントエンコードされた文字列をデコードする関数を実装します。

github.com

 

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()
}

 

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

f:id:kidani_a:20201219171920p:plain

 

[まとめ]

  • Kotlin/Nativeでパーセントエンコーディングされた文字列をデコードする処理を実装した
  • HTTPサーバーに組み込み、日本語のファイル名にアクセスできることを確認した
  • 実装中のコードは以下のリポジトリにまとめてある

github.com