Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 40 additions & 10 deletions fml/base32.cc
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,52 @@ std::pair<bool, std::string> Base32Encode(std::string_view input) {
const size_t encoded_length = (input.size() * 8 + 4) / 5;
output.reserve(encoded_length);

uint16_t bit_stream = (static_cast<uint8_t>(input[0]) << 8);
Base32EncodeConverter converter;
converter.Append(input[0]);
size_t next_byte_index = 1;
int free_bits = 8;

while (free_bits < 16) {
output.push_back(kEncoding[(bit_stream & 0xf800) >> 11]);
bit_stream <<= 5;
free_bits += 5;

if (free_bits >= 8 && next_byte_index < input.size()) {
free_bits -= 8;
bit_stream += static_cast<uint8_t>(input[next_byte_index++]) << free_bits;
while (converter.CanExtract()) {
output.push_back(kEncoding[converter.Extract()]);
if (converter.CanAppend() && next_byte_index < input.size()) {
converter.Append(static_cast<uint8_t>(input[next_byte_index++]));
}
}

if (converter.BitsAvailable() > 0) {
output.push_back(kEncoding[converter.Peek()]);
}

return {true, output};
}

static constexpr signed char kDecodeMap[] = {
// starting from ASCII 50 '2'
26, 27, 28, 29, 30, 31, -1, -1, -1, -1, -1, -1, -1, -1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25};

static constexpr int kDecodeMapSize =
sizeof(kDecodeMap) / sizeof(kDecodeMap[0]);

std::pair<bool, std::string> Base32Decode(const std::string& input) {
std::string result;
Base32DecodeConverter converter;
for (char c : input) {
int map_index = c - '2';
if (map_index < 0 || map_index >= kDecodeMapSize ||
kDecodeMap[map_index] == -1) {
return {false, result};
}
converter.Append(kDecodeMap[map_index]);
if (converter.CanExtract()) {
result.push_back(converter.Extract());
}
}
if (converter.Peek() != 0) {
// The padding should always be zero. Return false if not.
return {false, result};
}
return {true, result};
}

} // namespace fml
40 changes: 40 additions & 0 deletions fml/base32.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,49 @@
#include <string_view>
#include <utility>

#include "flutter/fml/logging.h"

namespace fml {

template <int from_length, int to_length, int buffer_length>
class BitConverter {
public:
void Append(int bits) {
FML_DCHECK(bits < (1 << from_length));
FML_DCHECK(CanAppend());
lower_free_bits_ -= from_length;
buffer_ |= (bits << lower_free_bits_);
}

int Extract() {
FML_DCHECK(CanExtract());
int result = Peek();
buffer_ = (buffer_ << to_length) & mask_;
lower_free_bits_ += to_length;
return result;
}

int Peek() const { return (buffer_ >> (buffer_length - to_length)); }
int BitsAvailable() const { return buffer_length - lower_free_bits_; }
bool CanAppend() const { return lower_free_bits_ >= from_length; }
bool CanExtract() const { return BitsAvailable() >= to_length; }

private:
static_assert(buffer_length >= 2 * from_length);
static_assert(buffer_length >= 2 * to_length);
static_assert(buffer_length < sizeof(int) * 8);

static constexpr int mask_ = (1 << buffer_length) - 1;

int buffer_ = 0;
int lower_free_bits_ = buffer_length;
};

using Base32DecodeConverter = BitConverter<5, 8, 16>;
using Base32EncodeConverter = BitConverter<8, 5, 16>;

std::pair<bool, std::string> Base32Encode(std::string_view input);
std::pair<bool, std::string> Base32Decode(const std::string& input);

} // namespace fml

Expand Down
48 changes: 48 additions & 0 deletions fml/base32_unittest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#include "flutter/fml/base32.h"
#include "gtest/gtest.h"

#include <iostream>

TEST(Base32Test, CanEncode) {
{
auto result = fml::Base32Encode("hello");
Expand Down Expand Up @@ -36,3 +38,49 @@ TEST(Base32Test, CanEncode) {
ASSERT_EQ(result.second, "NBSWYTDP");
}
}

TEST(Base32Test, CanEncodeDecodeStrings) {
std::vector<std::string> strings = {"hello", "helLo", "", "1", "\0"};
for (size_t i = 0; i < strings.size(); i += 1) {
auto encode_result = fml::Base32Encode(strings[i]);
ASSERT_TRUE(encode_result.first);
auto decode_result = fml::Base32Decode(encode_result.second);
ASSERT_TRUE(decode_result.first);
const std::string& decoded = decode_result.second;
std::string decoded_string(decoded.data(), decoded.size());
ASSERT_EQ(strings[i], decoded_string);
}
}

TEST(Base32Test, DecodeReturnsFalseForInvalideInput) {
// "B" is invalid because it has a non-zero padding.
std::vector<std::string> invalid_inputs = {"a", "1", "9", "B"};
for (const std::string& input : invalid_inputs) {
auto decode_result = fml::Base32Decode(input);
if (decode_result.first) {
std::cout << "Base32Decode should return false on " << input << std::endl;
}
ASSERT_FALSE(decode_result.first);
}
}

TEST(Base32Test, CanDecodeSkSLKeys) {
std::vector<std::string> inputs = {
"CAZAAAACAAAAADQAAAABKAAAAAJQAAIA7777777777776EYAAEAP777777777777AAAAAABA"
"ABTAAAAAAAAAAAAAAABAAAAAGQAGGAA",
"CAZAAAICAAAAAAAAAAADOAAAAAJQAAIA777777Y4AAKAAEYAAEAP777777777777EAAGMAAA"
"AAAAAAAAAAACQACNAAAAAAAAAAAAAAACAAAAAPAAMMAA",
"CAZACAACAAAABAYACAAAAAAAAAJQAAIADQABIAH777777777777RQAAOAAAAAAAAAAAAAABE"
"AANQAAAAAAAAAAAAAAYAAJYAAAAAAAANAAAQAAAAAAAEAAAHAAAAAAAAAAAAAAANAAAQAAAA"
"AAAFIADKAAAAAAAAAAAAAAACAAAAAZAAMMAA"};
for (const std::string& input : inputs) {
auto decode_result = fml::Base32Decode(input);
if (!decode_result.first) {
std::cout << "Base32Decode should return true on " << input << std::endl;
}
ASSERT_TRUE(decode_result.first);
auto encode_result = fml::Base32Encode(decode_result.second);
ASSERT_TRUE(encode_result.first);
ASSERT_EQ(encode_result.second, input);
}
}