| // 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 "jumbf/convert_json.h" | |
| #include <cstddef> | |
| #include <cstdint> | |
| #include <memory> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/log/log.h" | |
| #include "absl/status/status.h" | |
| #include "absl/status/statusor.h" | |
| #include "absl/strings/escaping.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/str_format.h" | |
| #include "absl/strings/string_view.h" | |
| #include "cppbor/cppbor.h" | |
| #include "cppbor/cppbor_parse.h" | |
| #include "jumbf/box.h" | |
| #include "nlohmann/json.hpp" | |
| #include "nlohmann/json_fwd.hpp" | |
| namespace jumbf { | |
| namespace { | |
| using Json = ::nlohmann::json; | |
| std::string EncodePrefixedBase64(absl::string_view raw) { | |
| return absl::StrCat("b64'", absl::Base64Escape(raw), "'"); | |
| } | |
| 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()); | |
| } | |
| cppbor::Map BstrToB64(cppbor::Map* map); | |
| 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. | |
| absl::StatusOr<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()) { | |
| return absl::InvalidArgumentError( | |
| absl::StrCat("CBOR parsing failed: ", error)); | |
| } | |
| if (new_position != | |
| reinterpret_cast<const uint8_t*>(raw.data() + raw.length())) { | |
| return absl::InvalidArgumentError( | |
| "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); | |
| } | |
| return absl::InvalidArgumentError( | |
| absl::StrCat("Input CBOR item is ", item->type(), | |
| "; expected array (128) or map (160).")); | |
| } | |
| class AllExceptionParser | |
| : public nlohmann::detail::json_sax_dom_parser< | |
| Json, nlohmann::detail::iterator_input_adapter<const char*>> { | |
| public: | |
| explicit AllExceptionParser(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) { | |
| errors_.push_back(absl::StrFormat("%s", ex.what())); | |
| return false; | |
| } | |
| std::vector<std::string> errors() { return errors_; } | |
| private: | |
| std::vector<std::string> errors_; | |
| }; | |
| } // namespace | |
| void to_json(Json& j, const DescriptionBox& box) { | |
| if (box.label.has_value()) { | |
| j["label"] = box.label.value(); | |
| } | |
| if (box.id.has_value()) { | |
| j["id"] = box.id.value(); | |
| } | |
| if (box.unvalidated_hash.has_value()) { | |
| j["hash"] = box.unvalidated_hash.value(); | |
| } | |
| j["uuid"] = box.type_uuid.ToString(); | |
| j["requestable"] = box.requestable; | |
| for (const auto& box : box.private_content) { | |
| j["private_content"].push_back(box); | |
| } | |
| j["_meta"]["_type"] = "description"; | |
| } | |
| void to_json(Json& j, const SuperBox& box) { | |
| j["description"] = box.description; | |
| for (const auto& box : box.contents) { | |
| j["contents"].push_back(box); | |
| } | |
| j["_meta"]["_type"] = "super"; | |
| } | |
| void to_json(Json& j, const UnknownBox& box) { | |
| j["type"] = box.type; | |
| j["_meta"]["_type"] = "unknown"; | |
| j["_meta"]["_payload_size"] = box.payload.size(); | |
| } | |
| void to_json(Json& j, const CborBox& box) { | |
| j["_meta"]["_type"] = "cbor"; | |
| j["_meta"]["_payload_size"] = box.payload.size(); | |
| absl::StatusOr<std::string> encoded = BstrToB64(box.payload); | |
| if (!encoded.ok()) { | |
| j["_meta"]["_error"] = encoded.status().ToString(); | |
| return; | |
| } | |
| AllExceptionParser parser(j["cbor"]); | |
| auto ia = nlohmann::detail::input_adapter(*encoded); | |
| nlohmann::detail::binary_reader<Json, decltype(ia), AllExceptionParser> | |
| reader(std::move(ia), Json::input_format_t::cbor); | |
| reader.sax_parse(Json::input_format_t::cbor, &parser, /*strict=*/true, | |
| /*tag_handler=*/Json::cbor_tag_handler_t::ignore); | |
| if (!parser.errors().empty()) { | |
| j["_meta"]["_error"] = parser.errors(); | |
| } | |
| } | |
| void to_json(Json& j, const SerializedSuperbox& box) { | |
| j["_meta"]["_type"] = "serialized_superbox"; | |
| j["_meta"]["_serialized_size"] = box.serialized.size(); | |
| } | |
| void to_json(Json& j, const EmbeddedFileDescriptionBox& box) { | |
| j["media_type"] = box.media_type; | |
| j["external"] = box.external; | |
| if (box.file_name.has_value()) { | |
| j["file_name"] = box.file_name.value(); | |
| } | |
| j["_meta"]["_type"] = "embedded_file_description"; | |
| } | |
| void to_json(Json& j, const BinaryDataBox& box) { | |
| j["_meta"]["_type"] = "binary"; | |
| j["_meta"]["_payload_size"] = box.payload.size(); | |
| } | |
| void to_json(Json& j, const C2paSaltBox& box) { | |
| j["_meta"]["_type"] = "c2pa_salt"; | |
| j["_meta"]["_salt_size"] = box.salt.size(); | |
| } | |
| void to_json(Json& j, const JsonBox& box) { | |
| AllExceptionParser parser(j["json"]); | |
| Json::sax_parse(box.payload, &parser, Json::input_format_t::json); | |
| if (!parser.errors().empty()) { | |
| j["_meta"]["_error"] = parser.errors(); | |
| } | |
| j["_meta"]["_type"] = "json"; | |
| j["_meta"]["_payload_size"] = box.payload.size(); | |
| } | |
| void to_json(Json& j, const ContentBox& box) { | |
| if (box.Holds<jumbf::SuperBox>()) { | |
| j = box.Get<jumbf::SuperBox>(); | |
| } else if (box.Holds<jumbf::UnknownBox>()) { | |
| j = box.Get<jumbf::UnknownBox>(); | |
| } else if (box.Holds<jumbf::CborBox>()) { | |
| j = box.Get<jumbf::CborBox>(); | |
| } else if (box.Holds<jumbf::SerializedSuperbox>()) { | |
| j = box.Get<jumbf::SerializedSuperbox>(); | |
| } else if (box.Holds<jumbf::EmbeddedFileDescriptionBox>()) { | |
| j = box.Get<jumbf::EmbeddedFileDescriptionBox>(); | |
| } else if (box.Holds<jumbf::BinaryDataBox>()) { | |
| j = box.Get<jumbf::BinaryDataBox>(); | |
| } else if (box.Holds<jumbf::C2paSaltBox>()) { | |
| j = box.Get<jumbf::C2paSaltBox>(); | |
| } else if (box.Holds<jumbf::JsonBox>()) { | |
| j = box.Get<jumbf::JsonBox>(); | |
| } else { | |
| j["_meta"]["_error"] = "Contents Not Accounted For"; | |
| } | |
| } | |
| } // namespace jumbf |