| // 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/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 absl::StrReplaceAll(descriptor->full_name(), {{".", "::"}}); |
| } |
| |
| 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 |