以前書いたBase64エンコーダ
D言語で書く "イケメンなコード" ~ Base64エンコーダをワンライナーで書く ~
のバグを直した時に、「あっ、まだデコーダを書いてないや」と思ったので書きました。
例によって型推論が効いているので引数の型は省略しました。
今回は記事の末尾にこれのもととなった、ヒューマンリーダブル(?)な通常の実装も載せておきます。
tinyBase64Decoder.d
import std.algorithm,
std.string,
std.ascii,
std.range,
std.conv;
R delegate(Args) Z(R, Args...)(R delegate(R delegate(Args), Args) f){
return (Args args) => f(Z(f), args);
}
ubyte[] decode(string data) {
return
(convb =>
(table =>
(converted =>
(cLen =>
(quotients =>
(qLen =>
(qLen / 2).iota.map!(i =>
(buf =>
(bu2 =>
((b0, b1) =>
(b0 << 4 ^ b1) & 0xff
)(bu2(buf[0]), bu2(buf[1]))
)((string bs) => bs.parse!ubyte(2))
)(quotients[(i * 2)..((i + 1) * 2)])
).array.to!(ubyte[])
)(quotients.length)
)(
(cLen / 4).iota.map!(i =>
converted[(i * 4)..((i + 1) * 4)]
).array
)
)(converted.length)
)(
data.filter!(x => x != '=').map!(e =>
table[e.to!string]
).array.join
)
)((charset =>
assocArray(
zip(
charset,
charset.length.iota.map!(i =>
(e =>
e.length == 6
? e
: repeat("0", 6 - e.length).join ~ e
)(convb(i, 2)))
))
)((uppercase ~ lowercase ~ digits ~ "+/").split(string.init))
)
)((ulong N, int base) =>
(convbM =>
convbM(N, N, [], base)
)(Z((string delegate(ulong, ulong, ulong[], int base) convbM, ulong N, ulong tmp, ulong[] stack, int base) =>
tmp ? convbM(N, tmp / base, stack ~ (tmp % base), base) : stack.reverse.map!(e => e.to!string).join)));
}
unittest {
string base = "ABCDEFG",
encoded = encode(base);
assert(base == decode(encoded));
}
エンコードに比べて割りと楽でした(ビット演算で少しだけ躓いてしまったので頑張りたい)
では、ヒューマンリーダブルなコードを掲載しておきます(解説も簡単ながら付けておきました):
import std.algorithm,
std.string,
std.ascii,
std.range,
std.conv;
ubyte[] decodeX(string data) {
//Base64のデコード用の変換テーブル(Base64文字列 -> 6bit)
string[string] table =
((string delegate(ulong, int) convb) =>
((string[] charset) =>
assocArray(
zip(
charset,
charset.length.iota.map!((ulong i) =>
((string e) =>
e.length == 6
? e
: repeat("0", 6 - e.length).join ~ e
)(convb(i, 2)))
)
)
)((uppercase ~ lowercase ~ digits ~ "+/").split(string.init))
)((ulong N, int base) =>
(convbM =>
convbM(N, N, [], base)
)(Z((string delegate(ulong, ulong, ulong[], int base) convbM, ulong N, ulong tmp, ulong[] stack, int base) =>
tmp ? convbM(N, tmp / base, stack ~ (tmp % base), base) : stack.reverse.map!(e => e.to!string).join)));
//変換表に従い、Base64文字列を6bitに変換し、それを結合
string converted =
data.filter!(x => x != '=').map!(e =>
table[e.to!string]
).array.join;
//4bitずつにconvertedを切り分けるための配列
string[] quotients;
//デコードした結果を格納する
ubyte[] decoded;
//4文字(4bit)分取り出して、配列に追加する(切り出し)
foreach (i; (converted.length / 4).iota) {
string str = converted[(i * 4)..((i + 1) * 4)];
quotients ~= str;
}
//隣接する4bit * 2の8bitからバイナリを復元
foreach (i; (quotients.length / 2).iota) {
//隣接する4bit * 2を取り出す
string[] buf = quotients[(i * 2)..((i + 1) * 2)];
//文字列化された数字を2進数として解釈し、10進数に変換する
auto bu2 = ((string bs) => bs.parse!ubyte(2));
ubyte b0 = bu2(buf[0]),
b1 = bu2(buf[1]);
//bit演算してバイナリに戻す
decoded ~= (b0 << 4 ^ b1) & 0xff;
}
/*
Base64のデコードは数多くの実装が存在するが(検索すると多くの人がすでに実装&解説をしています)、その多くは6bit * 4 = 24bit を 8bit * 3として扱うためにbit演算をしているような気がします。
しかし、隣接する4bit * 2から計算する方法を用いれば、6bit * 4とするために`quotients` % 4(このコードのquotientsに相当するもの)を別に考える必要がなくなります(たぶん... 自信はないですけど)。
*/
return decoded;
}