diff --git a/Package.swift b/Package.swift index c4975a01..fb154e45 100644 --- a/Package.swift +++ b/Package.swift @@ -416,6 +416,7 @@ let package = Package( "internal/crypto/BUILD", "internal/crypto/BUILD.gn", "internal/crypto_cros/BUILD", + "internal/encoding/BUILD", "internal/interop/BUILD", "internal/weave/BUILD", "internal/weave/sockets/BUILD", @@ -505,6 +506,7 @@ let package = Package( "internal/crypto_cros/sha2_unittest.cc", "internal/crypto_cros/signature_verifier_unittest.cc", "internal/crypto_cros/symmetric_key_unittest.cc", + "internal/encoding/base85_test.cc", "internal/data/leveldb_data_set_test.cc", "internal/data/memory_data_set_test.cc", "internal/flags/nearby_flags_test.cc", diff --git a/internal/encoding/BUILD b/internal/encoding/BUILD new file mode 100644 index 00000000..104bc121 --- /dev/null +++ b/internal/encoding/BUILD @@ -0,0 +1,39 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +licenses(["notice"]) + +package(default_visibility = ["//:__subpackages__"]) + +cc_library( + name = "base85", + srcs = ["base85.cc"], + hdrs = ["base85.h"], + compatible_with = ["//buildenv/target:non_prod"], + deps = [ + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:string_view", + ], +) + +cc_test( + name = "base85_test", + srcs = ["base85_test.cc"], + deps = [ + ":base85", + "@com_github_protobuf_matchers//protobuf-matchers", + "@com_google_absl//absl/strings", + "@com_google_googletest//:gtest_main", + ], +) diff --git a/internal/encoding/base85.cc b/internal/encoding/base85.cc new file mode 100644 index 00000000..51cc8b08 --- /dev/null +++ b/internal/encoding/base85.cc @@ -0,0 +1,190 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "internal/encoding/base85.h" + +#include +#include +#include +#include +#include + +namespace nearby { +namespace encoding { +namespace { + +constexpr int kBase85CharsNumber = 85; +constexpr uint8_t kBase85Zero = 'z'; +constexpr unsigned char kBase85EncodeChars[] = { + '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', + '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', + ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', + 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u'}; + +constexpr uint8_t kBase85LastEncodeChar = + kBase85EncodeChars[kBase85CharsNumber - 1]; + +constexpr uint8_t kBase85DecodeCharsCount[] = {0, 0, 1, 2, 3}; + +static uint8_t kBase85DecodeChars[256]; + +void InitBase85DecodeChars() { + if (kBase85DecodeChars[kBase85LastEncodeChar]) { + return; + } + + std::fill(kBase85DecodeChars, kBase85DecodeChars + 256, 0xff); + + for (uint8_t i = 0; i < kBase85CharsNumber; ++i) { + kBase85DecodeChars[kBase85EncodeChars[i]] = i; + } +} +} // namespace + +std::string Base85Encode(const std::string& input, bool padding) { + size_t result_size = + 5 * (input.size() >> 2) + (((input.size() % 4) * 5 + 3) >> 2); + if (padding) { + result_size = 5 * (input.size() >> 2) + ((input.size() % 4 != 0) ? 5 : 0); + } + + std::string result; + result.resize(result_size); + char* result_buffer = result.data(); + char* result_buffer_end = result.data() + result.size(); + + size_t bytes = input.size(); + const char* data = input.data(); + while (bytes) { + int count; + uint32_t value = 0; + for (count = 24; count >= 0; count -= 8) { + uint32_t ch = *data++; + value |= ch << count; + if (--bytes == 0) break; + } + + if (value == 0) { + // special case, use 'z' to encode it + result_buffer[0] = kBase85Zero; + ++result_buffer; + continue; + } + + int changed_count = 0; + for (count = 4; count >= 0; count--) { + int val = value % 85; + value /= 85; + if (result_buffer + count < result_buffer_end) { + result_buffer[count] = kBase85EncodeChars[val]; + ++changed_count; + } + } + + result_buffer += changed_count; + } + + if (result_buffer != result_buffer_end) { + // special case, may include z encoding + return std::string(result.data(), result_buffer - result.data()); + } + + return result; +} + +std::optional Base85Decode(const std::string& input) { + InitBase85DecodeChars(); + + size_t length = 0; + size_t pos = 0; + for (; pos < input.size();) { + if (input[pos] == kBase85Zero) { + // special zero case + length += 4; + ++pos; + continue; + } + if (pos + 5 > input.size()) { + size_t remain = input.size() - pos; + if (remain == 1) { + return std::nullopt; + } + length += kBase85DecodeCharsCount[remain]; + break; + } + pos += 5; + length += 4; + } + + std::string result; + result.resize(length); + + const char* input_buffer = input.data(); + const char* input_buffer_end = input_buffer + input.size(); + char* result_buffer = result.data(); + char* result_buffer_end = result_buffer + result.size(); + + while (length) { + int count = 4; + int decoded_char; + uint8_t input_char; + uint32_t value = 0; + do { + if (input_buffer < input_buffer_end) { + input_char = *input_buffer++; + // handle special case 'z' + if (count == 4 && input_char == kBase85Zero) { + for (int i = 0; i < count; ++i) { + *result_buffer++ = 0x00; + } + length -= 4; + continue; + } + + decoded_char = kBase85DecodeChars[input_char]; + if (decoded_char == 0xff) return std::nullopt; + } else { + decoded_char = kBase85LastEncodeChar; + } + value = value * 85 + decoded_char; + } while (--count); + if (input_buffer < input_buffer_end) { + input_char = *input_buffer++; + decoded_char = kBase85DecodeChars[input_char]; + if (decoded_char == 0xff) return std::nullopt; + } else { + decoded_char = kBase85LastEncodeChar; + } + /* Detect overflow. */ + if (0xffffffff / 85 < value || 0xffffffff - decoded_char < (value *= 85)) + return std::nullopt; + value += decoded_char; + + count = (length < 4) ? length : 4; + length -= count; + do { + value = (value << 8) | (value >> 24); + if (result_buffer < result_buffer_end) { + *result_buffer++ = value; + } + } while (--count); + } + + return result; +} + +} // namespace encoding +} // namespace nearby diff --git a/internal/encoding/base85.h b/internal/encoding/base85.h new file mode 100644 index 00000000..5a4f45cf --- /dev/null +++ b/internal/encoding/base85.h @@ -0,0 +1,34 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_INTERNAL_ENCODING_BASE85_H_ +#define THIRD_PARTY_NEARBY_INTERNAL_ENCODING_BASE85_H_ + +#include +#include + +namespace nearby { +namespace encoding { + +// Encodes the input string to Base85. If pad is true, the output string will +// not be truncated. +std::string Base85Encode(const std::string& input, bool padding = false); + +// Decodes the input string from Base85. +std::optional Base85Decode(const std::string& input); + +} // namespace encoding +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_INTERNAL_ENCODING_BASE85_H_ diff --git a/internal/encoding/base85_test.cc b/internal/encoding/base85_test.cc new file mode 100644 index 00000000..6a00f8df --- /dev/null +++ b/internal/encoding/base85_test.cc @@ -0,0 +1,78 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "internal/encoding/base85.h" + +#include + +#include "gtest/gtest.h" + +namespace nearby { +namespace encoding { +namespace { + +TEST(Base85Test, EncodeAndDecodeString) { + EXPECT_EQ(Base85Encode(""), ""); + EXPECT_EQ(Base85Decode(Base85Encode("")), ""); + EXPECT_EQ(Base85Encode("b"), "@K"); + EXPECT_EQ(Base85Decode(Base85Encode("b")), "b"); + EXPECT_EQ(Base85Encode("ab"), "@:B"); + EXPECT_EQ(Base85Decode(Base85Encode("ab")), "ab"); + EXPECT_EQ(Base85Encode("xyz"), "G^4T"); + EXPECT_EQ(Base85Decode(Base85Encode("xyz")), "xyz"); + EXPECT_EQ(Base85Encode("true"), "FE2M8"); + EXPECT_EQ(Base85Decode(Base85Encode("true")), "true"); + EXPECT_EQ(Base85Encode("ab", /*padding=*/true), "@:B3:"); + std::string expected_decoded_string = "ab"; + expected_decoded_string.push_back('\0'); + expected_decoded_string.push_back('\0'); + EXPECT_EQ(Base85Decode(Base85Encode("ab", /*padding=*/true)), + expected_decoded_string); + EXPECT_EQ(Base85Encode("hello,world"), "BOu!rD_-*NEbo7"); + EXPECT_EQ(Base85Decode("BOu!rD_-*NEbo7"), "hello,world"); + EXPECT_EQ(Base85Encode("abcdefgh"), "@:E_WAS,Rg"); + EXPECT_EQ(Base85Decode(Base85Encode("abcdefgh")), "abcdefgh"); +} + +TEST(Base85Test, EncodeAndDecodeBytes) { + std::string data; + data.push_back(0x00); + EXPECT_EQ(Base85Encode(data), "z"); + std::string expected_data; + expected_data.push_back(0x00); + expected_data.push_back(0x00); + expected_data.push_back(0x00); + expected_data.push_back(0x00); + EXPECT_EQ(Base85Decode(Base85Encode(data)), expected_data); + data.clear(); + data.push_back(0x00); + data.push_back(0x01); + EXPECT_EQ(Base85Encode(data), "!!*"); + data.clear(); + data.push_back(0x00); + data.push_back(0x01); + data.push_back(0xff); + EXPECT_EQ(Base85Encode(data), "!!3*"); + EXPECT_EQ(Base85Decode(Base85Encode(data)), data); +} + +TEST(Base85Test, InvalideEncodedString) { + EXPECT_FALSE(Base85Decode("abhjki")); + EXPECT_FALSE(Base85Decode("abz")); + EXPECT_FALSE(Base85Decode("u`qL[")); +} + +} // namespace +} // namespace encoding +} // namespace nearby