-- FILE:     BASE64.E
-- PURPOSE:  Base64 routines for Euphoria 3.1.1.
-- AUTHOR:   Shian Lee
-- VERSION:  1.00, October/20/2019
-- LANGUAGE: Euphoria version 3.1.1 (http://www.RapidEuphoria.com)
-- PLATFORM: Any
-- LICENCE:  Free. Use at your own risk.
-- NOTE:     * See the attached reference manual MACHINE2.DOC for details.
--           * Created using edu 2.33 (RapidEuphoria311.com archive).
--           * See also unittest/base64.ex
--           * More about Base64: https://en.wikipedia.org/wiki/Base64
--                                https://base64.guru/
-- HISTORY:  v1.00 - initial version
-----------------------------------------------------------------------------



--bookmark: Global Constants;

-- PURPOSE: supported variants of Base64 encoding
-- NOTICE:  these are the serial indexes for B_VARIANT sequence.
-- CREDIT:  https://en.wikipedia.org/wiki/Base64#Implementations_and_history
global constant B64_MAIN = 1,
		B64_MIME = 2,
		B64_UTF7 = 3,
		B64_URL  = 4,
		B64_IMAP = 5,
		B64_PEM  = 6,
		B64_YUI  = 7,
		B64_PI1  = 8,
		B64_PI2  = 9,
		B64_URL2 = 10



--bookmark: Local Constants;

-- PURPOSE: supported variants of Base64 encoding
constant
    -- Base64 64-characters set: A-Z, a-z, 0-9, +/ (only +/ is changeable)
    CHRSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
    -- index: 0                         26                        52        62

    -- end-of-line separators
    CRLF = {13, 10},    -- CR+LF (standard EOL for Internet)

    -- default values: no end-of-line, no padding
    EMPTY = {},
    DEF_EOL_LEN_PAD = {EMPTY, 0, {EMPTY, EMPTY}},

    -- B_VARIANT sub-sequences indexes
    I_SET = 1,  -- Base64 character set (64 characters string-sequence)
    I_EOL = 2,  -- end-of-line string-sequence (e.g. CRLF)
    I_LEN = 3,  -- length of Base64 line (excluding end-of-line)
    I_PAD = 4   -- padding sequence; e.g. {"==", '='}, {"--", '-'}, etc


constant B_VARIANT = {
    -- B64_MAIN [1] base64 for RFC 4648 (previously, RFC 3548; standard);
	{CHRSET, EMPTY, 0, {"==", '='}},

    -- B64_MIME [2] Base64 transfer encoding for MIME (RFC 2045)
	{CHRSET, CRLF, 76, {"==", '='}},

    -- B64_UTF7 [3] Base64 for UTF-7 (RFC 2152)
	{CHRSET} & DEF_EOL_LEN_PAD,

    -- B64_URL [4] base64url URL|filename-safe (RFC 4648 [5]), (optional pad '=')
	{CHRSET[1..62] & "-_"} & DEF_EOL_LEN_PAD,

    -- B64_IMAP [5] Base64 encoding for IMAP mailbox names (RFC 3501)
	{CHRSET[1..62] & "+,"} & DEF_EOL_LEN_PAD,

    -- B64_PEM [6] Base64 for Privacy-Enhanced Mail (PEM; RFC 1421; deprecated)
	{CHRSET, CRLF, 64, {"==", '='}},

    -- B64_YUI [7] Y64 URL-safe Base64 from YUI Library, (optional pad '-')
	{CHRSET[1..62] & "._"} & DEF_EOL_LEN_PAD,

    -- B64_PI1 [8] Program identifier Base64 variant 1 (non-standard)
	{CHRSET[1..62] & "_-"} & DEF_EOL_LEN_PAD,

    -- B64_PI2 [9] Program identifier Base64 variant 2 (non-standard)
	{CHRSET[1..62] & "._"} & DEF_EOL_LEN_PAD,

    -- B64_URL2 [10] Freenet URL-safe Base64 (non-standard), (optional pad '=')
	{CHRSET[1..62] & "~-"} & DEF_EOL_LEN_PAD
}



constant
    -- factors for calculations
    SHx16 = #10000,     -- power(2, 16), shift 16-bits
    SHx8  = #100,       -- power(2,  8), shift 8-bits
    SHx18 = #40000,     -- power(2, 18), shift 18-bits
    SHx12 = #1000,      -- power(2, 12), shift 12-bits
    SHx6  = #40,        -- power(2,  6), shift 6-bits
    SHx0  = #1,         -- power(2,  0), shift 0-bits

    SH_6BITS = {SHx18, SHx12, SHx6, SHx0},
    SH_8BITS = {SHx16, SHx8, SHx0},

    MASK6  = #3F,       -- 0b00111111
    MASK8  = #FF        -- 0b11111111



--bookmark: Global Base64 Routines;

-- PURPOSE: encode a string-sequence into Base64
-- DETAILS: variant is one of the global constants above (B64_MAIN, etc)
--          s is binary (ASCII 0-255) byte-string-sequence
-- RETURN:  sequence; Base64 encoded ASCII byte-string-sequence
-- EXAMPLE: s = base64_encode(B64_MAIN, "Man")  -- s is "TWFu"
--          s = base64_decode(B64_MAIN, "TWFu") -- s is "Man"
global function base64_encode(integer variant, sequence s)
    integer line_len, len, pad, num, count
    sequence va, cs, b, r

    va = B_VARIANT[variant] -- use specific Base64 variant
    cs = va[I_SET]          -- Base64 character set
    line_len = va[I_LEN]    -- if line_len = 0 then don't add end-of-line

    len = length(s)
    pad = remainder(len, 3) -- length of padding; ensures multiple of 4 length
    count = 0               -- count length of line in 4 bytes chunks
    r = EMPTY               -- return value

    for i = 1 to len - pad by 3 do
	-- encode s (3-bytes chunk) to Base64 (4-bytes)
	num = s[i] * SHx16 + s[i + 1] * SHx8 + s[i + 2] -- 3-bytes to int
	b = and_bits(floor(num / SH_6BITS), MASK6) + 1  -- int to 4-bytes
	r &= cs[b[1]] & cs[b[2]] & cs[b[3]] & cs[b[4]]

	-- add line separator if required (*not* to last line)
	if line_len then
	    count += 4
	    if count = line_len then
		count = 0
		if i < (len - 2) then -- not last line?
		    r &= va[I_EOL]
		end if
	    end if
	end if
    end for

    -- encode last 1 to 2 bytes, and pad to multiple of 4 if required
    if pad then
	num = s[len - pad + 1] * SHx16
	if pad = 2 then
	    num += s[len] * SHx8
	end if
	b = and_bits(floor(num / SH_6BITS), MASK6) + 1  -- int to 4-bytes
	r &= cs[b[1]] & cs[b[2]]    -- ignore extra #00 byte(s)
	if pad = 2 then
	    r &= cs[b[3]]
	end if
	r &= va[I_PAD][pad] -- pad if required
    end if

    return r
end function



-- PURPOSE: decode a Base64 string-sequence
-- DETAILS: variant is one of the global constants above (B64_MAIN, etc)
--          s is Base64 encoded ASCII byte-string-sequence
-- NOTE:    any non-Base64 character is *discarded* (ignored)
-- RETURN:  sequence; binary (ASCII 0-255) byte-string-sequence
global function base64_decode(integer variant, sequence s)
    integer  len, num, count, c
    sequence va, cs, r, b

    va = B_VARIANT[variant] -- use specific Base64 variant
    cs = va[I_SET]          -- Base64 character set

    len = length(s)
    b = repeat(0, 4)        -- Base64 4-bytes chunk
    count = 0               -- count 4-bytes
    r = EMPTY               -- return value

    for i = 1 to len do
	c = s[i]

	-- (binary) index in Base64 character set, i.e. c = find(s[i], cs) - 1
	-- note: if...then block is a bit faster then a single find() line.
	if c >= 'A' and c <= 'Z' then
	    count += 1
	    b[count] = c - 'A'      -- index 'A' is 0
	elsif c >= 'a' and c <= 'z' then
	    count += 1
	    b[count] = c - 71       -- index 'a' is 26
	elsif c >= '0' and c <= '9' then
	    count += 1
	    b[count] = c + 4        -- index '0' is 52
	elsif c = cs[63] then
	    count += 1
	    b[count] = 62           -- (0-based index)
	elsif c = cs[64] then
	    count += 1
	    b[count] = 63           -- (0-based index)
--      else
--          -- any non-Base64 character is discarded (in all variants)
	end if

	-- decode Base64 4-bytes chunk
	if count = 4 then
	    -- 4 six-bit-bytes to int
	    num = b[1] * SHx18 + b[2] * SHx12 + b[3] * SHx6 + b[4]
	    -- 24-bit-int to 3-bytes
	    r &= and_bits(floor(num / SH_8BITS), MASK8)
	    count = 0
	end if
    end for

    -- decode last 1 to 3 bytes
    if count then
	-- 1-3 six-bit-bytes to int
	num = b[1] * SHx18
	if count >= 2 then
	    num += b[2] * SHx12
	end if
	if count = 3 then
	    num += b[3] * SHx6
	end if
	-- less then 24-bit-int to 1-3-bytes
	r &= and_bits(floor(num / SH_8BITS[1..count - 1]), MASK8)
    end if

    return r
end function



-- End of file.

