jc/encoding/base64.jai

253 lines
6.9 KiB
Text

base64_encode :: (str: string, $for_url := false) -> string, bool {
enc, ok := base64_encode(str.([]u8), for_url);
return enc.(string), ok;
}
base64_decode :: (str: string) -> string, bool {
enc, ok := base64_decode(str.([]u8));
return enc.(string), ok;
}
base64_encode :: ($$data: []u8, $for_url := false) -> []u8, bool {
if data.count == 0 return .[], false;
#if for_url {
Character_Set :: Encoding_Url;
}
else {
Character_Set :: Encoding_Standard;
}
padded := strings.contains(data.(string), Padding_Character);
encoded := basic.NewArray(encoded_length(data.count, padded), u8, true);
src_idx := 0;
dst_idx := 0;
src_max := (data.count / 3) * 3;
while src_idx < src_max {
value: u64;
parts := (*value).(*u8);
(parts + 0).* = xx data[src_idx + 2];
(parts + 1).* = xx data[src_idx + 1];
(parts + 2).* = xx data[src_idx + 0];
encoded[dst_idx + 0] = Character_Set[value >> 18 & 0x3F];
encoded[dst_idx + 1] = Character_Set[value >> 12 & 0x3F];
encoded[dst_idx + 2] = Character_Set[value >> 6 & 0x3F];
encoded[dst_idx + 3] = Character_Set[value & 0x3F];
src_idx += 3;
dst_idx += 4;
}
remaining := data.count - src_idx;
if !remaining then return encoded, true;
value := data[src_idx + 0].(u64) << 16;
if remaining == 2 {
value |= data[src_idx + 1].(u64) << 8;
}
encoded[dst_idx + 0] = Character_Set[value >> 18 & 0x3F];
encoded[dst_idx + 1] = Character_Set[value >> 12 & 0x3F];
if remaining == {
case 2;
if padded {
encoded[dst_idx + 2] = Padding_Character;
encoded[dst_idx + 3] = Padding_Character;
}
case 1;
encoded[dst_idx + 2] = Character_Set[value >> 6 & 0x3F];
if padded {
encoded[dst_idx + 3] = Padding_Character;
}
}
return encoded, true;
}
base64_decode :: (data: []u8) -> []u8, bool {
if !data.count return .[], false;
lookup :: (c: u8) -> u8 #expand {
if c >= #char "A" && c <= #char "Z" return c - #char "A";
if c >= #char "a" && c <= #char "z" return c - 71;
if c >= #char "0" && c <= #char "9" return c + 4;
if c == #char "+" || c == #char "-" return 62;
if c == #char "/" || c == #char "_" return 63;
return 255;
}
a4_to_a3 :: () #expand {
`a3[0] = (`a4[0] << 2) + ((`a4[1] & 0x30) >> 4);
`a3[1] = ((`a4[1] & 0xF) << 4) + ((`a4[2] & 0x3C) >> 2);
`a3[2] = ((`a4[2] & 0x3) << 6) + `a4[3];
}
padding := 0;
{
i := data.count - 1;
while i >= 0 {
if data[i] == Padding_Character {
padding += 1;
}
else {
break;
}
i -= 1;
}
}
a3: [3]u8;
a4: [4]u8;
i := 0;
offset := 0;
decoded := basic.NewArray(((6 * data.count) / 8) - padding, u8, true);
for 0..data.count - 1 {
chr := data[it];
if chr == Padding_Character break;
a4[i] = chr;
i += 1;
if i == 4 {
for 0..3 a4[it] = lookup(a4[it]);
a4_to_a3();
for 0..2 {
decoded.data[offset] = a3[it];
offset += 1;
}
i = 0;
}
}
if i > 0 {
for 0..3 a4[it] = lookup(a4[it]);
a4_to_a3();
for 0..i - 1 {
(decoded.data + offset + it).* = a3[it];
}
}
return decoded, true;
}
#scope_file;
Padding_Character :: #char "=";
Encoding_Url :: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
Encoding_Standard :: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
encoded_length :: (count: int, with_padding := false) -> int {
if with_padding then return (count + 2) / 3 * 4;
return (count * 8 + 5) / 6;
}
#scope_file;
basic :: #import "Basic"; // @future
strings :: #import "String"; // @future
// ----------------------------------------------------------
// TESTS
// ----------------------------------------------------------
#if RUN_TESTS #run {
test.run("encodes", (t) => {
str :: "Hello, World";
encoded, ok := base64_encode(str);
test.expect(t, ok, "'%' did not properly encode!", str);
expected :: "SGVsbG8sIFdvcmxk";
test.expect(t, encoded == expected, "wanted: '%', received: '%'", expected, encoded);
});
test.run("decodes", (t) => {
encoded_str :: "SGVsbG8sIFdvcmxk";
decoded, ok := base64_decode(encoded_str);
test.expect(t, ok, "'%' did not properly decode!", encoded_str);
expected :: "Hello, World";
test.expect(t, decoded == expected, "wanted: '%', received: '%'", expected, decoded);
});
test.run("encodes/decodes padding", (t) => {
str :: "VkdocGN5QnBjeUJ6YjIxbGRHaHBibWNnYlc5eVpTQmpiMjF3YkdWNExDQnBkQ0J6YUc5MWJHUWdhR0YyWlNCd1lXUmthVzVuUHc9PQ==";
first_decode, f_ok := base64_decode(str);
test.expect(t, f_ok, "first decode failed!");
re_encode, r_ok := base64_encode(first_decode);
test.expect(t, r_ok, "re-encode failed!");
second_decode, s_ok := base64_decode(re_encode);
test.expect(t, s_ok, "second decode failed!");
for 0..first_decode.count - 1 {
test.expect(t, first_decode[it] == second_decode[it], "Decoded byte '%' did not match expected output", it);
}
});
test.run("encodes/decodes struct", (t) => {
Foo :: struct {
first: u64;
second: u64;
third: u64;
forth: u64;
}
foo: Foo;
foo.first = 123;
foo.second = 456;
foo.third = 789;
foo.forth = 100;
enc_builder: basic.String_Builder;
basic.print_to_builder(*enc_builder, "%,%,%,%", foo.first, foo.second, foo.third, foo.forth);
encoded, e_ok := base64_encode(basic.builder_to_string(*enc_builder));
test.expect(t, e_ok, "Encode of structure failed!");
decoded, d_ok := base64_decode(encoded);
test.expect(t, d_ok, "Decode of encoded structure failed!");
parts := strings.split(decoded, ",");
test.expect(t, parts.count == 4, "Invalid number of parts after decode: %", parts.count);
bar: Foo = ---;
{
ptr := (*bar).(*u8);
info := Foo.(*Type_Info_Struct);
for info.members {
value, ok := strings.parse_int(*parts[it_index], u64);
test.expect(t, ok, "Integer parse of value % ('%') failed!", it_index, parts[it_index]);
offset := (ptr + it.offset_in_bytes).(*u64);
offset.*= value;
}
}
test.expect(t, foo.first == bar.first, "Foo.first (%, %) didn't match between encoding/decoding!", foo.first, bar.first);
test.expect(t, foo.second == bar.second, "Foo.second (%, %) didn't match between encoding/decoding!", foo.second, bar.second);
test.expect(t, foo.third == bar.third, "Foo.third (%, %) didn't match between encoding/decoding!", foo.third, bar.third);
test.expect(t, foo.forth == bar.forth, "Foo.forth (%, %) didn't match between encoding/decoding!", foo.forth, bar.forth);
});
}