Swiftでmultipart/form-dataのパーサを作ったよ
きっかけ
iOSアプリ内でWebサーバを立ち上げて、multipart/form-dataなファイルアップロードを実装しようとしたんだけど
使ってるWebサーバアプリにちょうどいい感じのパーサがなくて、自分で書きました。
そもそもスマホってクライアント側になることあれど、サーバ側になることはあんまりないから情報が少ないのよね…
コード
Swift 5のつもりで書いてます。
考え方
testと書かれたfile.txtをアップロードすると
base64エンコードして送信されてくるので、デコードしたあとはこんな感じのデータ。
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary5aJB0dB2B5PV7h8H ------WebKitFormBoundary5aJB0dB2B5PV7h8H Content-Disposition: form-data; name="userfile"; filename="file.txt" Content-Type: text/plain test ------WebKitFormBoundary5aJB0dB2B5PV7h8H Content-Disposition: form-data; name="submit" Upload ------WebKitFormBoundary5aJB0dB2B5PV7h8H--
boundary=のところの ----WebKitFormBoundary5aJB0dB2B5PV7h8H
文字列がセパレータで複数ファイルをアップロードされたときはこれでパースする。
バイナリファイルの苦悩
テキストファイルの処理は非常に簡単。
丸ごとbase64デコードして、セパレータでパースするだけ。
ところがバイナリファイルをアップロードするときはそうはいかない。
Data(base64Encoded: encodedString!)でデコードするとnilが返ってきてしまう。
恐らく、バイナリファイルのデータ部がデコードできなくて失敗としてnilが返ってきているのだと思う。
全体でデコードできないとなると、Data型で扱っての先頭から走査して順次、デコードしていく必要が出てくる。
なのでこんな感じでData型のまんま、boundaryでパースする処理を入れてファイルごとに分割して
extension Data { func splitByData(boundary: String) -> [Data]? { guard let boundaryData = boundary.data(using: .utf8) else { return nil } // With a line feed let withLinefeed = boundary + LINEFEED guard let withLinefeedData = withLinefeed.data(using: .utf8) else { return nil } var chunks: [Data] = [] var pos = startIndex while let r = self[pos...].range(of: withLinefeedData) { if r.lowerBound > pos { chunks.append(self[pos..<r.lowerBound]) } pos = r.upperBound } if pos < endIndex { if let endRange = self[pos...].range(of: boundaryData) { chunks.append(self[pos..<endRange.lowerBound]) } } return chunks } }
ファイルごとのデータを一行ごとに丁寧に処理していくことになる。
private static func parseMultipartBodyData(bodyData: Data) -> MultipartBodyFile? { // Line Feed let lineFeed = LINEFEED.data(using: .utf8) // Split a first line guard let firstLineRange = bodyData.range(of: lineFeed!) else { return nil } let firstLine = bodyData[bodyData.startIndex..<firstLineRange.lowerBound] // Split a second line guard let secondLineRange = bodyData[firstLineRange.upperBound...].range(of: lineFeed!) else { return nil } let secondLine = bodyData[firstLineRange.upperBound..<secondLineRange.lowerBound] // Get a third line range guard let thirdLineRange = bodyData[secondLineRange.upperBound...].range(of: lineFeed!) else { return nil } guard let fileName = parseFileName(firstLine: firstLine) else { return nil } guard let contentType = parseContentType(secondLine: secondLine) else { return nil } return MultipartBodyFile(contentType: contentType, fileName: fileName, data: bodyData[thirdLineRange.upperBound...]) }
普段何気なく使っているWebフレームワークの高機能さとありがたさが身に沁みます。
余談
Webサーバ自体はこのTelegraphがサクッとつかえて、高機能なのでありがたく使っております。
GitHub - Building42/Telegraph: Secure Web Server for iOS, tvOS and macOS