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 #exists(TESTS) #run { test :: #import "jc/meta/test"; 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); }); }