要旨
Fortan で base64 の encode/decode を行うルーチンを作った。 Bit 演算を使わないことで Big/Little endian に依らずどちらでもいけるようにした(つもり)。
Base64 とは
Base64 についてはネットで調べてもらった方が早いと思われますが、簡単に言えばバイナリを含むデータを ASCII の可読文字に変換して、可読文字しか使えないようなメールなどでのバイナリファイルの扱いを可能にするものです。
基本的に 3byte = 24bit を 4 つの 6bit に分割して、その 6bit に対して 2^6 = 64 の可読文字を割り当てるものです。24bit からの端数の扱いにひと工夫があって埋草に更に 1 文字用いるので 65 種の ASCII 文字に変換され、全体のサイズは 4/3 倍に膨らみます。
詳細は以下で
Fortan での実装
アルゴリズムは単純ですが、ビットをより分けねばなりません。
bit 演算を用いるには、一度整数に変換しなければなりませんが、そうなると Endian の問題が生じます。1 byte 整数を用いることも考えられますますが、今度は処理系がサポートしているかの問題が生じます。
そこでここでは bit 演算を用いずに、achar/iachar による文字と整数の変換と算術演算での bit 切り出しを行うことにしました。結果的に少し冗長なプログラムになっています。
プログラム
変換・逆変換とも、文字列を引数とし文字列を返す関数として実装しました。
チェックのため 10~10^7 文字の非可読文字を含む文字列を乱数で生成し、base64 にencode、続いて decode を行い、元の文字列が復元されたかを調べています。
module base64_m
implicit none
character, parameter :: b64(0:63) = &
transfer('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', ' ', size = 64)
contains
function b64enc(text) result(res)
character(len = *), intent(in) :: text
character(len = :), allocatable :: res
integer :: i, n, ires, nres, m1, m2, m3, k1, k2, k3, k4
n = len(text)
nres = ceiling(n / 3.0) * 4
allocate(character(len = nres):: res)
ires = 1
do i = 1, (n / 3) * 3, 3
m1 = iachar(text(i :i ))
m2 = iachar(text(i+1:i+1))
m3 = iachar(text(i+2:i+2))
k1 = m1 / 4
k2 = m2 / 16 + mod(m1, 4) * 16
k3 = m3 / 64 + mod(m2, 16) * 4
k4 = mod(m3, 64)
res(ires:ires + 3) = b64(k1) // b64(k2) // b64(k3) // b64(k4)
ires = ires + 4
end do
!
select case(mod(n, 3))
case(1)
m1 = iachar(text(i:i))
k1 = m1 / 4
k2 = mod(m1, 4) * 16
res(ires:ires+3) = b64(k1) // b64(k2) // '=='
case(2)
m1 = iachar(text(i :i ))
m2 = iachar(text(i+1:i+1))
k1 = m1 / 4
k2 = m2 / 16 + mod(m1, 4) * 16
k3 = mod(m2, 16) * 4
res(ires:ires+3) = b64(k1) // b64(k2) // b64(k3) // '='
case default
end select
end function b64enc
function b64dec(text) result(res)
character(*), intent(in) :: text
character(:), allocatable :: res
integer :: i, j, n, no, m1, m2, m3, m4, k1, k2, k3
n = len(text)
no = (n / 4) * 3
if (text(n :n ) == '=') no = no - 1
if (text(n-1:n-1) == '=') no = no - 1
allocate(character(len = no):: res)
j = 1
do i = 1, n-4, 4
m1 = findloc(b64, text(i :i ), dim = 1) - 1
m2 = findloc(b64, text(i+1:i+1), dim = 1) - 1
m3 = findloc(b64, text(i+2:i+2), dim = 1) - 1
m4 = findloc(b64, text(i+3:i+3), dim = 1) - 1
k1 = m1 * 4 + m2 / 16
k2 = mod(m2, 16) * 16 + m3 / 4
k3 = mod(m3, 4) * 64 + m4
res(j :j ) = achar(k1)
res(j+1:j+1) = achar(k2)
res(j+2:j+2) = achar(k3)
j = j + 3
end do
!
m1 = findloc(b64, text(i :i ), dim = 1) - 1
m2 = findloc(b64, text(i+1:i+1), dim = 1) - 1
k1 = m1 * 4 + m2 / 16
res(j:j) = achar(k1)
if (text(i+2:i+2) == '=') return
m3 = findloc(b64, text(i+2:i+2), dim = 1) - 1
k2 = mod(m2, 16) * 16 + m3 / 4
res(j+1:j+1) = achar(k2)
if (text(i+3:i+3) == '=') return
m4 = findloc(b64, text(i+3:i+3), dim = 1) - 1
k3 = mod(m3, 4) * 64 + m4
res(j+2:j+2) = achar(k3)
end function b64dec
end module base64_m
program base64enc
use base64_m
implicit none
character(:), allocatable :: buff, buff64, buff64d
integer :: i, j, k
real, allocatable :: x(:)
call random_seed()
do i = 0, 7
k = 10**i
allocate(x(k))
allocate(character(len = k)::buff)
call random_number(x)
forall(j=1:k) buff(j:j) = achar( int( 256*x(j) ) )
buff64 = b64enc(buff)
buff64d = b64dec(buff64)
if (buff == buff64d) then
print *, k, 'OK: '!, buff64
else
print *, k, '*** ', buff64
stop
end if
deallocate(x, buff, buff64, buff64d)
end do
end program base64enc
実行結果
1 OK:
10 OK:
100 OK:
1000 OK:
10000 OK:
100000 OK:
1000000 OK:
10000000 OK:
10^7 byte ~ 10Mbyte の時、encode/decode に 2 秒程度かかっています。
ここには示しませんが適当な文字列を変換し、ネット上のオンライン base64 変換サイトでの結果との比較も行って確認しています。
まとめ
Fortran で base64 encode/decode のプログラムを書いた。Endian に依らない実装にした(つもりだが、little endian でしか確認していない)。