読者です 読者をやめる 読者になる 読者になる

ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。

Base 64 エンコーディングと改行 (line feed) の話

Ruby RFC

Base 64 エンコーディングすると、(使用している言語やライブラリによりますが) 勝手に改行 (line feed) が含まれてしまって困るということがしばしばあります。 例えば、RubyBase64::encode64 メソッドを使用した場合も改行が含まれてしまいます。

本記事では、以下の内容について説明します。

  • Ruby において改行を含めない Base64 エンコーディングを行う方法
  • (Ruby に限らず一般的な話として) Base64 エンコーディングと改行 (line feed) の関係について

Ruby で改行が含まれない Base 64 エンコーディングを行う方法

Ruby で base 64 エンコーディングをしようとするとき、テンプレート文字列 'm' を使って pack することが多いかと思います。 しかし、'm' を使った pack では何故か改行 (line feed) が途中に入ってきたりしてハマってしまったという人もいるのではないでしょうか。 Ruby には Base64 モジュールもあって、Base64::encode64 関数を使って base 64 エンコーディングもできるのですが、こっちにも改行が追加される問題はあって、あんまり嬉しくない感じでした (というか Base64::encode64 関数の中では 'm' の pack をしているので変わりがない)。

私も昔この line feed に悩まされたことがあって 「やだなー」 と思ってたんですが、Ruby 1.9 系には Base64::strict_encode64 という関数 が追加され、改行を含めない base 64 エンコードを行うことができるようになりました。 良いですね。

1.9 系の場合

1.9 系においては、上でも述べた Base64::strict_encode64 関数を使うのが改行を含めない base 64 エンコードの方法として最もわかりやすいと思います。

# coding: UTF-8
require 'base64'
target_str = 'この文字列を base 64 エンコードします'
Base64::strict_encode64( target_str ) #=> "44GT44Gu5paH5a2X5YiX44KSIGJhc2UgNjQg44Ko44Oz44Kz44O844OJ44GX44G+44GZ"

# 以下でもよい
[target_str].pack('m0') #=> "44GT44Gu5paH5a2X5YiX44KSIGJhc2UgNjQg44Ko44Oz44Kz44O844OJ44GX44G+44GZ"

Base64::strict_encode64 の実装としては、Array#pack メソッドにテンプレート文字列として 'm0' を渡す形になっています。 テンプレート文字 'm' の後ろの数字は改行の文字数を表しますが、1.9 系では 0 を渡すことで line feed を入れさせないという指定になります (1.8 系では無効)。

pack メソッドを使って base 64 エンコードしてもいいでしょうが、見た目のわかりやすさからすると Base64 モジュールを使った方が良さそうです。

1.8 系の場合 (または 1.8 系と 1.9 系に対応する必要がある場合)

1.8 系では 1.9 系で使える方法が使えませんので、'm' で pack するなり Base64::encode64 するなりした後で改行文字を取り除く必要があります。

target_str = 'この文字列を base 64 エンコードします'
[target_str].pack('m').delete!("\n") #=> "44GT44Gu5paH5a2X5YiX44KSIGJhc2UgNjQg44Ko44Oz44Kz44O844OJ44GX44G+44GZ"

# pack の代わりに Base64::encode64 関数を使う場合
require 'base64'
Base64::encode64( target_str ).delete("\n") #=> "44GT44Gu5paH5a2X5YiX44KSIGJhc2UgNjQg44Ko44Oz44Kz44O844OJ44GX44G+44GZ"

Base 64 エンコーディングと line feed の話

RFC 4648 には、以下のようなことが書いてありました。

3.1. Line Feeds in Encoded Data

MIME [4] is often used as a reference for base 64 encoding. However, MIME does not define "base 64" per se, but rather a "base 64 Content-Transfer-Encoding" for use within MIME. As such, MIME enforces a limit on line length of base 64-encoded data to 76 characters. MIME inherits the encoding from Privacy Enhanced Mail (PEM) [3], stating that it is "virtually identical"; however, PEM uses a line length of 64 characters. The MIME and PEM limits are both due to limits within SMTP.

Implementations MUST NOT add line feeds to base-encoded data unless the specification referring to this document explicitly directs base encoders to add line feeds after a specific number of characters.

RFC 4648 - The Base16, Base32, and Base64 Data Encodings

要は、Base 64 エンコーディングに関するリファレンスとして RFC 2045 がよく使用されているが、これは base 64 エンコーディングそのものに関して規定しているわけではなく MIME の中で使われる方法について書かれているものであり、そのために 1 行あたりの文字数制限が生じてしまっている、ということのようです。 Base 64 エンコーディングされたデータ自体については、明示的に指定されない限り line feed を含めてはならないものとされています。

RubyRFC 2045 を参照して line feed を入れるようになっていたぽいですね。 Ruby 1.9 系の Base64::strict_encode64 関数は RFC 4648 を参照しているので line feed は入らないようになっています。

Ruby の ML ではここらへんで議論されていた模様。

  • [ruby-dev:30445] openssl enc -base64 -d で base64 を decodeしてみると、78 文字あたりで制限があり...
  • [ruby-dev:30457] RFC2045 の 6.8. Base64 Content-Transfer-Encoding の Table 1 に続くパラグラフに...
  • [ruby-dev:30458] これは見落としていましたm(_ _)m そこで手をつけた以上はと思い、調べてみると今は base16/32/64 専用の rfcがあって...