「iPhone 設定をアプリから操作してらくらく環境構築」のQRコードの内容の解析編

この記事は iOSDC 2019 LT の「[iPhone 設定をアプリから操作してらくらく環境構築](https://speakerdeck.com/orgachem/easy-provisioning-iphone-using-configuration-profile-via-ios-app)」の補足です。


# 背景

iOS 11 から Wi-Fi を QR コードで設定できるようになって便利ですよね。ただ、事情があって iOS 11 へあげられない方もいると思います。その方向けに iOS 11 未満でも使える Wi-Fi 設定ライブラリ [WiFiQRCodeKit](https://github.com/Kuniwak/WiFiQRCodeKit) を作成してみました。その過程はちょっと面倒でした。


# ちょっと面倒の詳細
## QR コードの読み取り

まず、標準ライブラリで QR コードを読み取れるようになるのは iOS 11 からです。

iOS では QR コードの読み取りライブラリが豊富です。そのため、ここでは苦労しませんでした。今回は [yannickl/QRCodeReader.swift](https://github.com/yannickl/QRCodeReader.swift) を使うことにしました。


## QR コードの形式調査

まず、QR コードにどのような形式で Wi-Fi の接続情報がエンコードされているのか調べることからはじめました。最終的に [zxing/zxing の Wiki](https://github.com/zxing/zxing/wiki/Barcode-Contents#wifi-network-config-android) にエンコード形式が書いてあるとわかりました。例えば、SSID が「mynetwork」、暗号化方式が WPA (or WPA2)、パスワードが「mypass」の Wi-Fi に接続する QR コードの内容は以下のような文字列です:

```
WIFI:T:WPA;S:mynetwork;P:mypass;;
```

前述の Wiki にも書いてある通り、これは太古に docomo が策定していた [MeCard の形式](https://web.archive.org/web/20111202054137/http://www.nttdocomo.co.jp/english/service/imode/make/content/barcode/function/application/addressbook/index.html)を真似ています。この形式の仕様は Internet Archive からしか見つからないのは趣深いですね:relaxed:。

さて、これから作成するライブラリではこの内容から接続情報を取り出さなければなりません。このときに必要になる技術が構文解析です。この構文解析の入力と出力のイメージは次のようなものです（あくまでイメージです）:

```swift
let reuslt = parseWiFiQRCode(text: "WIFI:T:WPA;S:mynetwork;P:mypass;;")

XCTAssertEqual(result.ssid, "mynetwork")
XCTAssertEqual(result.password, "mypass")
XCTAssertEqual(result.encrytionType, "WPA")
```

これだけを見ると、次のような愚直な実装が考えられるかもしれません:

```swift:StupidParser.swift
// 愚直な構文解析の実装（後述する一部のケースで壊れます）
func parseWiFiQRCode(text: String) {
    let content = text.replacingOccurences(of: "WIFI:", with: "")
    let fields = content
        .split(separator: ";", omittingEmptySubsequences: true)
        .map { field in field.split(":") }

    let ssid = fields.first { field in field[0] == "S" }
    let password = fields.first { field in field[0] == "P" }
    let encryptionType = fields.first { field in field[0] == "T" }

    return (ssid: ssid, password: password, encryptionType: encryptionType)
}
```

しかし、注意しなければならない点があります。SSID やパスワード は「`:`」や「`;`」を含む可能性があるのです[^1]。この場合は「`\:`」や「`\;`」のようにエスケープをすることになっているようです[^2]。このように、エスケープの規則が含まれるような場合に `split` を使ってしまうと、「`\;`」のような区切り位置ではない場所でも区切ってしまうため、不具合になってしまいます。このぐらいの例であれば、正規表現で頑張って抜き出すこともできるかもしれません。しかし、SSID やパスワードの出現順序は不定で、かつ暗号化方式は省略可能という仕様なため、その正規表現はかなり複雑なものになるはずです。頑張って書いてみるとこんな感じでしょうか：

```
/WIFI:(?:(T:(?:[^\\]|\\[:;\\])*)|(S:(?:[^\\]|\\[:;\\])*)|(P:(?:[^\\]|\\[:;\\])*);)+;/
```

ただし、この結果で抜き出された文字列にはまだエスケープが残っています。そのため、エスケープを外す処理をこの先に書かなければなりませんが、そこそこ面倒な処理になります[^3]。そうでなくとも、Swift における正規表現処理は面倒すぎて使いたくありません。

そこで、今回は [Monadic Parser Combinator](http://www.cs.nott.ac.uk/~pszgmh/monparsing.pdf) を使うことにしました。これは構文解析の処理のデザインパターンのひとつです。このデザインパターンによる実装は高速ではないものの、再利用が容易で静的型検査によく馴染む堅牢な書き方をできます。ただ、これを解説するとすごく長くなってしまうので割愛します。要するに、構文解析に一工夫がいることを説明したかったのです。

なんとかこれで Wi-Fi の接続情報を取得できるようになりました。


# 終わりに

たかが QR コードの内容の解析1つとっても、いろいろな知識が必要なのは辛いですね :cry: 


[^1]: 例えば SSID は 32 bytes の octet string なので、これらの記号や絵文字なども含められます。
[^2]: Wi-Fi の接続情報を QR コードにしてくれるいくつかのサービスは、このエスケープの実装をさぼっているようです。もし、QR コードでは自分の Wi-Fi に接続できない、という方はこれが原因かもしれません。
[^3]: `/\\([^:;\\))/$1/` でいいじゃん！と思ったあなたは `"a\\b"` を試してみるといいでしょう。
[^5]: 実用上は `String` ではなく、部分文字列を意味する標準型 [`Substring`](https://developer.apple.com/documentation/swift/substring) を使います。