Fortran で Base64 変換

# 要旨
Fortan　で base64 の encode/decode を行うルーチンを作った。　Bit 演算を使わないことで Big/Little endian に依らずどちらでもいけるようにした（つもり）。

# Base64 とは

Base６４ についてはネットで調べてもらった方が早いと思われますが、簡単に言えばバイナリを含むデータを ASCII の可読文字に変換して、可読文字しか使えないようなメールなどでのバイナリファイルの扱いを可能にするものです。

基本的に 3byte = 24bit を 4　つの 6bit に分割して、その 6bit に対して　2^6 = 64 の可読文字を割り当てるものです。24bit からの端数の扱いにひと工夫があって埋草に更に 1 文字用いるので ６５ 種の ASCII 文字に変換され、全体のサイズは 4/3 倍に膨らみます。

詳細は以下で

https://ja.wikipedia.org/wiki/Base64

# Fortan での実装

アルゴリズムは単純ですが、ビットをより分けねばなりません。
bit 演算を用いるには、一度整数に変換しなければなりませんが、そうなると Endian の問題が生じます。１ byte 整数を用いることも考えられますますが、今度は処理系がサポートしているかの問題が生じます。

そこでここでは bit 演算を用いずに、achar/iachar による文字と整数の変換と算術演算での bit 切り出しを行うことにしました。結果的に少し冗長なプログラムになっています。

## プログラム

変換・逆変換とも、文字列を引数とし文字列を返す関数として実装しました。

チェックのため 10~10^7 文字の非可読文字を含む文字列を乱数で生成し、base64 にencode、続いて　decode を行い、元の文字列が復元されたかを調べています。


```fortran
    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 に ２ 秒程度かかっています。

ここには示しませんが適当な文字列を変換し、ネット上のオンライン base64 変換サイトでの結果との比較も行って確認しています。

https://www.base64encode.org/

# まとめ

Fortran で base64 encode/decode のプログラムを書いた。Endian に依らない実装にした（つもりだが、little endian でしか確認していない）。