| // Copyright 2026 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 "testing/cbor_utils.h" | |
| #include <sys/types.h> | |
| #include <cstddef> | |
| #include <cstdint> | |
| #include <memory> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/log/check.h" | |
| #include "absl/log/log.h" | |
| #include "absl/strings/escaping.h" | |
| #include "absl/strings/match.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/string_view.h" | |
| #include "absl/strings/strip.h" | |
| #include "cppbor/cppbor.h" | |
| #include "cppbor/cppbor_parse.h" | |
| #include "nlohmann/json.hpp" | |
| namespace cbor { | |
| namespace { | |
| using Json = ::nlohmann::json; | |
| cppbor::Map B64ToBstr(cppbor::Map* map); | |
| cppbor::Map BstrToB64(cppbor::Map* map); | |
| constexpr absl::string_view kBase64Prefix = "b64'"; | |
| constexpr absl::string_view kBase64Suffix = "'"; | |
| class NoExceptionParser | |
| : public nlohmann::detail::json_sax_dom_parser< | |
| Json, nlohmann::detail::iterator_input_adapter<const char*>> { | |
| public: | |
| explicit NoExceptionParser(Json& j) | |
| : nlohmann::detail::json_sax_dom_parser< | |
| Json, nlohmann::detail::iterator_input_adapter<const char*>>( | |
| j, false) {} | |
| bool parse_error(std::size_t position, absl::string_view last_token, | |
| const Json::exception& ex) { | |
| error_ = ex.what(); | |
| return false; // Stop processing. | |
| } | |
| std::string error() { return error_; } | |
| private: | |
| std::string error_; | |
| }; | |
| std::string DecodePrefixedBase64(absl::string_view encoded) { | |
| auto stripped = absl::StripPrefix(encoded, kBase64Prefix); | |
| if (!absl::EndsWith(stripped, kBase64Suffix)) { | |
| LOG(FATAL) << "The base64-encoded string " << encoded | |
| << " does not end with the suffix " << kBase64Suffix; | |
| } | |
| stripped = absl::StripSuffix(stripped, kBase64Suffix); | |
| std::string decoded; | |
| if (!absl::Base64Unescape(stripped, &decoded)) { | |
| LOG(FATAL) << "Failed to decode base64: " << stripped; | |
| } | |
| return decoded; | |
| } | |
| std::string EncodePrefixedBase64(absl::string_view raw) { | |
| return absl::StrCat(kBase64Prefix, absl::Base64Escape(raw), kBase64Suffix); | |
| } | |
| std::string EncodeCbor(cppbor::Item* item) { | |
| std::vector<uint8_t> buf; | |
| buf.resize(item->encodedSize()); | |
| item->encode(buf.data(), buf.data() + buf.size()); | |
| return std::string(reinterpret_cast<const char*>(buf.data()), buf.size()); | |
| } | |
| // Recursively replaces all base64-encoded Tstr with their decoded values as | |
| // Bstr. | |
| cppbor::Array B64ToBstr(cppbor::Array* array) { | |
| cppbor::Array new_array; | |
| for (auto& item : *array) { | |
| if (auto s = item->asTstr(); | |
| s != nullptr && s->value().starts_with(kBase64Prefix)) { | |
| new_array.add(cppbor::Bstr(DecodePrefixedBase64(s->value()))); | |
| } else if (auto m = item->asMap(); m != nullptr) { | |
| new_array.add(B64ToBstr(m)); | |
| } else if (auto a = item->asArray(); a != nullptr) { | |
| new_array.add(B64ToBstr(a)); | |
| } else { | |
| new_array.add(std::move(item)); | |
| } | |
| } | |
| return new_array; | |
| } | |
| // Recursively replaces all base64-encoded Tstr with their decoded values as | |
| // Bstr. | |
| cppbor::Map B64ToBstr(cppbor::Map* map) { | |
| cppbor::Map new_map; | |
| for (auto& [key, value] : *map) { | |
| if (auto v = value->asTstr(); | |
| v != nullptr && v->value().starts_with(kBase64Prefix)) { | |
| new_map.add(std::move(key), | |
| cppbor::Bstr(DecodePrefixedBase64(v->value()))); | |
| } else if (auto m = value->asMap(); m != nullptr) { | |
| new_map.add(std::move(key), B64ToBstr(m)); | |
| } else if (auto v = value->asArray(); v != nullptr) { | |
| cppbor::Array new_array = B64ToBstr(v); | |
| new_map.add(std::move(key), std::move(new_array)); | |
| } else { | |
| new_map.add(std::move(key), std::move(value)); | |
| } | |
| } | |
| return new_map; | |
| } | |
| // Recursively replaces all base64-encoded Tstr with their decoded values as | |
| // Bstr. Supports only arrays and maps. | |
| std::string B64ToBstr(const uint8_t* begin, const uint8_t* end) { | |
| auto [item, new_position, error] = cppbor::parse(begin, end); | |
| if (!error.empty()) { | |
| LOG(FATAL) << "CBOR parsing failed: " << error; | |
| } | |
| if (new_position != end) { | |
| LOG(FATAL) << "Trailing bytes after the parsed CBOR item"; | |
| } | |
| if (auto map = item->asMap(); map != nullptr) { | |
| auto new_map = B64ToBstr(map); | |
| return EncodeCbor(&new_map); | |
| } else if (auto array = item->asArray(); array != nullptr) { | |
| auto new_array = B64ToBstr(array); | |
| return EncodeCbor(&new_array); | |
| } | |
| LOG(FATAL) << "Input CBOR item is " << item->type() | |
| << "; expected array (128) or map (160)."; | |
| } | |
| std::unique_ptr<cppbor::Item> UnwrapTag(std::unique_ptr<cppbor::Item> item) { | |
| while (item->asSemanticTag() != nullptr) { | |
| if (auto t = item->asTstr(); t != nullptr) return t->clone(); | |
| if (auto u = item->asUint(); u != nullptr) return u->clone(); | |
| if (auto i = item->asInt(); i != nullptr) return i->clone(); | |
| if (auto b = item->asBool(); b != nullptr) return b->clone(); | |
| if (auto f = item->asFloat(); f != nullptr) return f->clone(); | |
| if (auto d = item->asDouble(); d != nullptr) return d->clone(); | |
| if (auto s = item->asSimple(); s != nullptr) return s->clone(); | |
| break; | |
| } | |
| return item; | |
| } | |
| // Recursively replaces all Bstr with base64-encoded Tstr. | |
| cppbor::Array BstrToB64(cppbor::Array* array) { | |
| cppbor::Array new_array; | |
| for (auto& item_ref : *array) { | |
| auto item = UnwrapTag(std::move(item_ref)); | |
| if (auto s = item->asBstr(); s != nullptr) { | |
| absl::string_view sv(reinterpret_cast<const char*>(s->value().data()), | |
| s->value().size()); | |
| new_array.add(cppbor::Tstr(EncodePrefixedBase64(sv))); | |
| } else if (auto m = item->asMap(); m != nullptr) { | |
| new_array.add(BstrToB64(m)); | |
| } else if (auto a = item->asArray(); a != nullptr) { | |
| new_array.add(BstrToB64(a)); | |
| } else { | |
| new_array.add(std::move(item)); | |
| } | |
| } | |
| return new_array; | |
| } | |
| // Recursively replaces all Bstr with base64-encoded Tstr. | |
| cppbor::Map BstrToB64(cppbor::Map* map) { | |
| cppbor::Map new_map; | |
| for (auto& [key_ref, value_ref] : *map) { | |
| auto key = UnwrapTag(std::move(key_ref)); | |
| auto value = UnwrapTag(std::move(value_ref)); | |
| if (auto s = value->asBstr(); s != nullptr) { | |
| absl::string_view sv(reinterpret_cast<const char*>(s->value().data()), | |
| s->value().size()); | |
| new_map.add(std::move(key), cppbor::Tstr(EncodePrefixedBase64(sv))); | |
| } else if (auto m = value->asMap(); m != nullptr) { | |
| new_map.add(std::move(key), BstrToB64(m)); | |
| } else if (auto v = value->asArray(); v != nullptr) { | |
| cppbor::Array new_array = BstrToB64(v); | |
| new_map.add(std::move(key), std::move(new_array)); | |
| } else { | |
| new_map.add(std::move(key), std::move(value)); | |
| } | |
| } | |
| return new_map; | |
| } | |
| // Recursively replaces all Bstr with base64-encoded Tstr. Supports only arrays | |
| // and maps. | |
| std::string BstrToB64(absl::string_view raw) { | |
| auto [item, new_position, error] = | |
| cppbor::parse(reinterpret_cast<const uint8_t*>(raw.data()), raw.length()); | |
| if (!error.empty()) { | |
| LOG(FATAL) << "CBOR parsing failed: " << error; | |
| } | |
| if (new_position != | |
| reinterpret_cast<const uint8_t*>(raw.data() + raw.length())) { | |
| LOG(FATAL) << "Trailing bytes after the parsed CBOR item"; | |
| } | |
| if (auto map = item->asMap(); map != nullptr) { | |
| auto new_map = BstrToB64(map); | |
| return EncodeCbor(&new_map); | |
| } else if (auto array = item->asArray(); array != nullptr) { | |
| auto new_array = BstrToB64(array); | |
| return EncodeCbor(&new_array); | |
| } | |
| LOG(FATAL) << "Input CBOR item is " << item->type() | |
| << "; expected array (128) or map (160)."; | |
| } | |
| } // namespace | |
| std::string FromJson(absl::string_view json) { | |
| Json result; | |
| NoExceptionParser parser(result); | |
| if (!Json::sax_parse(json, &parser, Json::input_format_t::json)) { | |
| LOG(FATAL) << "JSON parse error: " << parser.error(); | |
| } | |
| std::vector<uint8_t> cbor = Json::to_cbor(result); | |
| return B64ToBstr(cbor.data(), cbor.data() + cbor.size()); | |
| } | |
| std::string ToJson(absl::string_view cbor) { | |
| std::string encoded = BstrToB64(cbor); | |
| Json result; | |
| NoExceptionParser parser(result); | |
| auto ia = nlohmann::detail::input_adapter(encoded); | |
| nlohmann::detail::binary_reader<Json, decltype(ia), NoExceptionParser> reader( | |
| std::move(ia), Json::input_format_t::cbor); | |
| if (!reader.sax_parse(Json::input_format_t::cbor, &parser, /*strict=*/true, | |
| /*tag_handler=*/Json::cbor_tag_handler_t::ignore)) { | |
| LOG(FATAL) << "CBOR parse error: " << parser.error(); | |
| } | |
| return result.dump(); | |
| } | |
| } // namespace cbor |