| // 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 |