| // 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 "cbor/cbor_generator.h" | |
| #include <memory> | |
| #include <string> | |
| #include "absl/base/no_destructor.h" | |
| #include "absl/container/flat_hash_map.h" | |
| #include "absl/container/flat_hash_set.h" | |
| #include "absl/strings/ascii.h" | |
| #include "absl/strings/match.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/str_replace.h" | |
| #include "absl/strings/string_view.h" | |
| #include "cbor/tags.pb.h" | |
| #include "google/protobuf/compiler/code_generator.h" | |
| #include "google/protobuf/compiler/cpp/names.h" | |
| #include "google/protobuf/descriptor.h" | |
| #include "google/protobuf/io/printer.h" | |
| #include "google/protobuf/io/zero_copy_stream.h" | |
| namespace cbor { | |
| namespace { | |
| std::string QualifiedCppName(const google::protobuf::Descriptor* descriptor) { | |
| return google::protobuf::compiler::cpp::QualifiedClassName(descriptor); | |
| } | |
| absl::string_view StripProtoExtension(absl::string_view filename) { | |
| if (absl::EndsWith(filename, ".proto")) { | |
| return filename.substr(0, filename.size() - 6); | |
| } | |
| return filename; | |
| } | |
| const cbor::Tags& GetTags(const google::protobuf::FieldDescriptor* field) { | |
| if (field->options().HasExtension(cbor::tags)) { | |
| return field->options().GetExtension(cbor::tags); | |
| } | |
| static const absl::NoDestructor<cbor::Tags> default_tags; | |
| return *default_tags; | |
| } | |
| std::string GetCborKey(const google::protobuf::FieldDescriptor* field) { | |
| const auto& tags = GetTags(field); | |
| if (!tags.name().empty()) { | |
| return std::string(tags.name()); | |
| } | |
| return std::string(field->name()); | |
| } | |
| const absl::flat_hash_map<google::protobuf::FieldDescriptor::Type, | |
| absl::string_view>& | |
| GetProtoTypeToCborType() { | |
| static const absl::NoDestructor<absl::flat_hash_map< | |
| google::protobuf::FieldDescriptor::Type, absl::string_view>> | |
| proto_type_to_cbor_type({ | |
| {google::protobuf::FieldDescriptor::TYPE_MESSAGE, "Map"}, | |
| {google::protobuf::FieldDescriptor::TYPE_STRING, "String"}, | |
| {google::protobuf::FieldDescriptor::TYPE_UINT64, "Uint64"}, | |
| {google::protobuf::FieldDescriptor::TYPE_INT64, "Int64"}, | |
| {google::protobuf::FieldDescriptor::TYPE_UINT32, "Uint64"}, | |
| {google::protobuf::FieldDescriptor::TYPE_INT32, "Int64"}, | |
| {google::protobuf::FieldDescriptor::TYPE_BOOL, "Bool"}, | |
| {google::protobuf::FieldDescriptor::TYPE_FLOAT, "Float"}, | |
| {google::protobuf::FieldDescriptor::TYPE_DOUBLE, "Double"}, | |
| {google::protobuf::FieldDescriptor::TYPE_BYTES, "ByteString"}, | |
| }); | |
| return *proto_type_to_cbor_type; | |
| } | |
| absl::string_view GetCborTypeName( | |
| const google::protobuf::FieldDescriptor* field) { | |
| const auto& tags = GetTags(field); | |
| if (tags.type() != cbor::Tags::TYPE_UNSPECIFIED) { | |
| switch (tags.type()) { | |
| case cbor::Tags::TYPE_UINT: | |
| return "Uint64"; | |
| case cbor::Tags::TYPE_NINT: | |
| return "Int64"; | |
| case cbor::Tags::TYPE_BYTE_STRING: | |
| return "ByteString"; | |
| case cbor::Tags::TYPE_TEXT_STRING: | |
| return "String"; | |
| default: | |
| break; | |
| } | |
| } | |
| auto it = GetProtoTypeToCborType().find(field->type()); | |
| if (it != GetProtoTypeToCborType().end()) { | |
| return it->second; | |
| } | |
| return ""; | |
| } | |
| void GenerateMessageHeader(const google::protobuf::Descriptor* desc, | |
| google::protobuf::io::Printer& printer) { | |
| printer.Emit({{"name", QualifiedCppName(desc)}}, R"cc( | |
| absl::Status ToProto(const cbor::MapView& map, $name$* proto); | |
| absl::StatusOr<std::unique_ptr<cppbor::Map>> FromProto(const $name$& proto); | |
| absl::StatusOr<std::unique_ptr<cppbor::Map>> FromProto( | |
| const $name$& proto, FromProtoOptions options); | |
| )cc"); | |
| for (int i = 0; i < desc->nested_type_count(); ++i) { | |
| GenerateMessageHeader(desc->nested_type(i), printer); | |
| } | |
| } | |
| void GenerateToProtoBody(const google::protobuf::Descriptor* desc, | |
| google::protobuf::io::Printer& printer) { | |
| printer.Emit({{"name", QualifiedCppName(desc)}}, R"cc( | |
| absl::Status ToProto(const cbor::MapView& map, $name$* proto) { | |
| )cc"); | |
| { | |
| auto indent = printer.WithIndent(); | |
| for (int i = 0; i < desc->field_count(); ++i) { | |
| const auto* field = desc->field(i); | |
| const auto& tags = GetTags(field); | |
| if (tags.mode() == cbor::Tags::MODE_WRITE_ONLY || | |
| tags.mode() == cbor::Tags::MODE_SKIP) { | |
| continue; | |
| } | |
| std::string key = GetCborKey(field); | |
| printer.Emit(R"cc( | |
| { | |
| )cc"); | |
| { | |
| auto scope_indent = printer.WithIndent(); | |
| if (field->is_repeated()) { | |
| if (field->type() == | |
| google::protobuf::FieldDescriptor::TYPE_MESSAGE) { | |
| printer.Emit({{"field", field->name()}, {"key", key}}, R"cc( | |
| auto array_view = map.GetOptionalArray("$key$"); | |
| )cc"); | |
| if (tags.is_required()) { | |
| printer.Emit({{"key", key}}, R"cc( | |
| if (!array_view.has_value()) { | |
| return absl::InvalidArgumentError( | |
| "Missing required field: " | |
| "$key$"); | |
| } | |
| )cc"); | |
| } | |
| if (tags.disallow_empty()) { | |
| printer.Emit({{"key", key}}, R"cc( | |
| if (array_view.has_value() && array_view->size() == 0) { | |
| return absl::InvalidArgumentError( | |
| "Field $key$ cannot be " | |
| "empty"); | |
| } | |
| )cc"); | |
| } | |
| printer.Emit({{"field", field->name()}}, R"cc( | |
| if (array_view.has_value()) { | |
| for (uint32_t i = 0; i < array_view->size(); ++i) { | |
| ABSL_ASSIGN_OR_RETURN(auto item, array_view->GetMap(i)); | |
| ABSL_RETURN_IF_ERROR(ToProto(std::move(item), proto->add_$field$())); | |
| } | |
| } | |
| )cc"); | |
| } else if (!GetCborTypeName(field).empty()) { | |
| absl::string_view type = GetCborTypeName(field); | |
| printer.Emit( | |
| {{"field", field->name()}, {"key", key}, {"type", type}}, R"cc( | |
| auto array_view = map.GetOptionalArray("$key$"); | |
| )cc"); | |
| if (tags.is_required()) { | |
| printer.Emit({{"key", key}}, R"cc( | |
| if (!array_view.has_value()) { | |
| return absl::InvalidArgumentError( | |
| "Missing required field: " | |
| "$key$"); | |
| } | |
| )cc"); | |
| } | |
| if (tags.disallow_empty()) { | |
| printer.Emit({{"key", key}}, R"cc( | |
| if (array_view.has_value() && array_view->size() == 0) { | |
| return absl::InvalidArgumentError( | |
| "Field $key$ cannot be " | |
| "empty"); | |
| } | |
| )cc"); | |
| } | |
| printer.Emit({{"field", field->name()}, {"type", type}}, R"cc( | |
| if (array_view.has_value()) { | |
| for (uint32_t i = 0; i < array_view->size(); ++i) { | |
| ABSL_ASSIGN_OR_RETURN(auto item, array_view->Get$type$(i)); | |
| proto->add_$field$(std::move(item)); | |
| } | |
| } | |
| )cc"); | |
| } | |
| } else { | |
| if (field->type() == | |
| google::protobuf::FieldDescriptor::TYPE_MESSAGE) { | |
| if (tags.is_required()) { | |
| printer.Emit({{"field", field->name()}, {"key", key}}, R"cc( | |
| auto map_view = map.GetOptionalMap("$key$"); | |
| if (!map_view.has_value()) { | |
| return absl::InvalidArgumentError( | |
| "Missing required field: " | |
| "$key$"); | |
| } | |
| ABSL_RETURN_IF_ERROR(ToProto(*map_view, proto->mutable_$field$())); | |
| )cc"); | |
| } else { | |
| printer.Emit({{"field", field->name()}, {"key", key}}, R"cc( | |
| auto map_view = map.GetOptionalMap("$key$"); | |
| if (map_view.has_value()) { | |
| ABSL_RETURN_IF_ERROR(ToProto(*map_view, proto->mutable_$field$())); | |
| } | |
| )cc"); | |
| } | |
| } else if (!GetCborTypeName(field).empty()) { | |
| absl::string_view type = GetCborTypeName(field); | |
| if (tags.is_required()) { | |
| printer.Emit( | |
| {{"field", field->name()}, {"key", key}, {"type", type}}, | |
| R"cc( | |
| auto val = map.GetOptional$type$("$key$"); | |
| if (!val.has_value()) { | |
| return absl::InvalidArgumentError( | |
| "Missing required " | |
| "field: $key$"); | |
| } | |
| proto->set_$field$(*val); | |
| )cc"); | |
| } else { | |
| printer.Emit( | |
| {{"field", field->name()}, {"key", key}, {"type", type}}, | |
| R"cc( | |
| auto val = map.GetOptional$type$("$key$"); | |
| if (val.has_value()) { | |
| proto->set_$field$(*val); | |
| } | |
| )cc"); | |
| } | |
| } | |
| } | |
| } | |
| printer.Emit(R"cc( | |
| } | |
| )cc"); | |
| } | |
| printer.Emit(R"cc( | |
| return absl::OkStatus(); | |
| )cc"); | |
| } | |
| printer.Emit(R"cc( | |
| } | |
| )cc"); | |
| for (int i = 0; i < desc->nested_type_count(); ++i) { | |
| GenerateToProtoBody(desc->nested_type(i), printer); | |
| } | |
| } | |
| void GenerateFromProtoBody(const google::protobuf::Descriptor* desc, | |
| google::protobuf::io::Printer& printer) { | |
| printer.Emit({{"name", QualifiedCppName(desc)}}, R"cc( | |
| absl::StatusOr<std::unique_ptr<cppbor::Map>> FromProto( | |
| const $name$& proto) { | |
| return FromProto(proto, FromProtoOptions{}); | |
| } | |
| absl::StatusOr<std::unique_ptr<cppbor::Map>> FromProto( | |
| const $name$& proto, FromProtoOptions options) { | |
| auto map = std::make_unique<cppbor::Map>(); | |
| )cc"); | |
| { | |
| auto indent = printer.WithIndent(); | |
| for (int i = 0; i < desc->field_count(); ++i) { | |
| const auto* field = desc->field(i); | |
| const auto& tags = GetTags(field); | |
| if (tags.mode() == cbor::Tags::MODE_READ_ONLY || | |
| tags.mode() == cbor::Tags::MODE_SKIP) { | |
| continue; | |
| } | |
| std::string key = GetCborKey(field); | |
| if (field->is_repeated()) { | |
| if (tags.is_required() && tags.disallow_empty()) { | |
| printer.Emit({{"field", field->name()}, {"key", key}}, R"cc( | |
| if (!options.skip_validation && proto.$field$_size() == 0) { | |
| return absl::InvalidArgumentError("Field $key$ cannot be empty"); | |
| } | |
| )cc"); | |
| } | |
| if (tags.is_required()) { | |
| printer.Emit(R"cc( | |
| { | |
| )cc"); | |
| } else { | |
| printer.Emit({{"field", field->name()}}, R"cc( | |
| if (proto.$field$_size() > 0) { | |
| )cc"); | |
| } | |
| if (field->type() == google::protobuf::FieldDescriptor::TYPE_MESSAGE) { | |
| printer.Emit({{"field", field->name()}, {"key", key}}, R"cc( | |
| auto array = std::make_unique<cppbor::Array>(); | |
| for (int i = 0; i < proto.$field$_size(); ++i) { | |
| ABSL_ASSIGN_OR_RETURN(auto item, FromProto(proto.$field$(i))); | |
| array->add(std::move(item)); | |
| } | |
| map->add("$key$", std::move(array)); | |
| )cc"); | |
| } else if (field->type() == | |
| google::protobuf::FieldDescriptor::TYPE_BYTES) { | |
| printer.Emit({{"field", field->name()}, {"key", key}}, R"cc( | |
| auto array = std::make_unique<cppbor::Array>(); | |
| for (int i = 0; i < proto.$field$_size(); ++i) { | |
| array->add(cppbor::Bstr(std::string(proto.$field$(i)))); | |
| } | |
| map->add("$key$", std::move(array)); | |
| )cc"); | |
| } else if (field->type() == | |
| google::protobuf::FieldDescriptor::TYPE_STRING) { | |
| printer.Emit({{"field", field->name()}, {"key", key}}, R"cc( | |
| auto array = std::make_unique<cppbor::Array>(); | |
| for (int i = 0; i < proto.$field$_size(); ++i) { | |
| array->add(cppbor::Tstr(std::string(proto.$field$(i)))); | |
| } | |
| map->add("$key$", std::move(array)); | |
| )cc"); | |
| } else if (field->type() == | |
| google::protobuf::FieldDescriptor::TYPE_FLOAT) { | |
| printer.Emit({{"field", field->name()}, {"key", key}}, R"cc( | |
| auto array = std::make_unique<cppbor::Array>(); | |
| for (int i = 0; i < proto.$field$_size(); ++i) { | |
| array->add(cppbor::Float(proto.$field$(i))); | |
| } | |
| map->add("$key$", std::move(array)); | |
| )cc"); | |
| } else if (field->type() == | |
| google::protobuf::FieldDescriptor::TYPE_DOUBLE) { | |
| printer.Emit({{"field", field->name()}, {"key", key}}, R"cc( | |
| auto array = std::make_unique<cppbor::Array>(); | |
| for (int i = 0; i < proto.$field$_size(); ++i) { | |
| array->add(cppbor::Double(proto.$field$(i))); | |
| } | |
| map->add("$key$", std::move(array)); | |
| )cc"); | |
| } else { | |
| printer.Emit({{"field", field->name()}, {"key", key}}, R"cc( | |
| auto array = std::make_unique<cppbor::Array>(); | |
| for (int i = 0; i < proto.$field$_size(); ++i) { | |
| array->add(proto.$field$(i)); | |
| } | |
| map->add("$key$", std::move(array)); | |
| )cc"); | |
| } | |
| printer.Emit(R"cc( | |
| } | |
| )cc"); | |
| } else { | |
| if (tags.is_required()) { | |
| printer.Emit({{"field", field->name()}, {"key", key}}, R"cc( | |
| if (!options.skip_validation && !proto.has_$field$()) { | |
| return absl::InvalidArgumentError( | |
| "Missing required field: " | |
| "$key$"); | |
| } | |
| )cc"); | |
| } | |
| if (field->type() == google::protobuf::FieldDescriptor::TYPE_MESSAGE) { | |
| printer.Emit({{"field", field->name()}, {"key", key}}, R"cc( | |
| if (proto.has_$field$()) { | |
| ABSL_ASSIGN_OR_RETURN(auto item, FromProto(proto.$field$())); | |
| map->add("$key$", std::move(item)); | |
| } | |
| )cc"); | |
| } else if (field->type() == | |
| google::protobuf::FieldDescriptor::TYPE_BYTES) { | |
| printer.Emit({{"field", field->name()}, {"key", key}}, R"cc( | |
| if (proto.has_$field$()) { | |
| map->add("$key$", cppbor::Bstr(std::string(proto.$field$()))); | |
| } | |
| )cc"); | |
| } else if (field->type() == | |
| google::protobuf::FieldDescriptor::TYPE_STRING) { | |
| printer.Emit({{"field", field->name()}, {"key", key}}, R"cc( | |
| if (proto.has_$field$()) { | |
| map->add("$key$", cppbor::Tstr(std::string(proto.$field$()))); | |
| } | |
| )cc"); | |
| } else if (field->type() == | |
| google::protobuf::FieldDescriptor::TYPE_FLOAT) { | |
| printer.Emit({{"field", field->name()}, {"key", key}}, R"cc( | |
| if (proto.has_$field$()) { | |
| map->add("$key$", cppbor::Float(proto.$field$())); | |
| } | |
| )cc"); | |
| } else if (field->type() == | |
| google::protobuf::FieldDescriptor::TYPE_DOUBLE) { | |
| printer.Emit({{"field", field->name()}, {"key", key}}, R"cc( | |
| if (proto.has_$field$()) { | |
| map->add("$key$", cppbor::Double(proto.$field$())); | |
| } | |
| )cc"); | |
| } else { | |
| printer.Emit({{"field", field->name()}, {"key", key}}, R"cc( | |
| if (proto.has_$field$()) { | |
| map->add("$key$", proto.$field$()); | |
| } | |
| )cc"); | |
| } | |
| } | |
| } | |
| printer.Emit(R"cc( | |
| return map; | |
| )cc"); | |
| } | |
| printer.Emit(R"cc( | |
| } | |
| )cc"); | |
| for (int i = 0; i < desc->nested_type_count(); ++i) { | |
| GenerateFromProtoBody(desc->nested_type(i), printer); | |
| } | |
| } | |
| void CollectUsedDeps( | |
| const google::protobuf::Descriptor* descriptor, | |
| const google::protobuf::FileDescriptor* current_file, | |
| absl::flat_hash_set<const google::protobuf::FileDescriptor*>& used_deps) { | |
| for (int i = 0; i < descriptor->field_count(); ++i) { | |
| const auto* f = descriptor->field(i); | |
| if (f->type() == google::protobuf::FieldDescriptor::TYPE_MESSAGE) { | |
| if (GetTags(f).mode() == cbor::Tags::MODE_SKIP) { | |
| continue; | |
| } | |
| const auto* m = f->message_type(); | |
| if (m->file() != current_file) { | |
| used_deps.insert(m->file()); | |
| } | |
| } | |
| } | |
| for (int i = 0; i < descriptor->nested_type_count(); ++i) { | |
| CollectUsedDeps(descriptor->nested_type(i), current_file, used_deps); | |
| } | |
| } | |
| void GenerateHeader(const google::protobuf::FileDescriptor* file, | |
| absl::string_view base_name, | |
| google::protobuf::compiler::GeneratorContext* context) { | |
| std::unique_ptr<google::protobuf::io::ZeroCopyOutputStream> h_stream( | |
| context->Open(absl::StrCat(base_name, ".cbor.h"))); | |
| google::protobuf::io::Printer h_printer(h_stream.get()); | |
| std::string guard = absl::AsciiStrToUpper(absl::StrReplaceAll( | |
| absl::StrCat(base_name, "_CBOR_H_"), {{"/", "_"}, {".", "_"}})); | |
| h_printer.Emit({{"guard", guard}}, R"cc( | |
| #ifndef $guard$ | |
| #define $guard$ | |
| )cc"); | |
| if (file->message_type_count() > 0) { | |
| h_printer.Emit(R"cc( | |
| #include <memory> | |
| #include "absl/status/status.h" | |
| #include "absl/status/statusor.h" | |
| #include "cbor/cbor.h" | |
| #include "cbor/options.h" | |
| #include "cppbor/cppbor.h" | |
| )cc"); | |
| } | |
| h_printer.Emit({{"base", base_name}}, R"cc( | |
| #include "$base$.pb.h" | |
| )cc"); | |
| for (int i = 0; i < file->public_dependency_count(); ++i) { | |
| absl::string_view pub_base = | |
| StripProtoExtension(file->public_dependency(i)->name()); | |
| h_printer.Emit({{"pub", pub_base}}, R"cc( | |
| #include "$pub$.cbor.h" // IWYU pragma: export | |
| )cc"); | |
| } | |
| h_printer.Emit(R"cc( | |
| namespace cbor { | |
| )cc"); | |
| for (int i = 0; i < file->message_type_count(); ++i) { | |
| GenerateMessageHeader(file->message_type(i), h_printer); | |
| } | |
| h_printer.Emit({{"guard", guard}}, R"cc( | |
| } // namespace cbor | |
| #endif // $guard$ | |
| )cc"); | |
| } | |
| void GenerateCc(const google::protobuf::FileDescriptor* file, | |
| absl::string_view base_name, | |
| google::protobuf::compiler::GeneratorContext* context) { | |
| std::unique_ptr<google::protobuf::io::ZeroCopyOutputStream> cc_stream( | |
| context->Open(absl::StrCat(base_name, ".cbor.cc"))); | |
| google::protobuf::io::Printer cc_printer(cc_stream.get()); | |
| cc_printer.Emit({{"base", base_name}}, R"cc( | |
| #include "$base$.cbor.h" | |
| )cc"); | |
| if (file->message_type_count() > 0) { | |
| cc_printer.Emit({{"base", base_name}}, R"cc( | |
| #include "$base$.pb.h" | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_macros.h" | |
| #include "absl/status/statusor.h" | |
| #include "cbor/cbor.h" | |
| #include "cppbor/cppbor.h" | |
| )cc"); | |
| } | |
| absl::flat_hash_set<const google::protobuf::FileDescriptor*> used_deps; | |
| for (int i = 0; i < file->message_type_count(); ++i) { | |
| CollectUsedDeps(file->message_type(i), file, used_deps); | |
| } | |
| for (const auto* dep : used_deps) { | |
| absl::string_view dep_base = StripProtoExtension(dep->name()); | |
| cc_printer.Emit({{"dep", dep_base}}, R"cc( | |
| #include "$dep$.cbor.h" | |
| )cc"); | |
| } | |
| cc_printer.Emit(R"cc( | |
| namespace cbor { | |
| )cc"); | |
| for (int i = 0; i < file->message_type_count(); ++i) { | |
| GenerateToProtoBody(file->message_type(i), cc_printer); | |
| GenerateFromProtoBody(file->message_type(i), cc_printer); | |
| } | |
| cc_printer.Emit(R"cc( | |
| } // namespace cbor | |
| )cc"); | |
| } | |
| } // namespace | |
| bool CborGenerator::Generate( | |
| const google::protobuf::FileDescriptor* file, const std::string& parameter, | |
| google::protobuf::compiler::GeneratorContext* context, | |
| std::string* error) const { | |
| absl::string_view base_name = StripProtoExtension(file->name()); | |
| GenerateHeader(file, base_name, context); | |
| GenerateCc(file, base_name, context); | |
| return true; | |
| } | |
| } // namespace cbor |