// Protocol Buffers - Google's data interchange format // Copyright 2008 Google Inc. All rights reserved. // https://developers.google.com/protocol-buffers/ // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following disclaimer // in the documentation and/or other materials provided with the // distribution. // * Neither the name of Google Inc. nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "google/protobuf/compiler/objectivec/names.h" #include #include #include #include #include #include #include #include #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" #include "absl/strings/ascii.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_split.h" #include "google/protobuf/compiler/code_generator.h" #include "google/protobuf/compiler/objectivec/line_consumer.h" #include "google/protobuf/compiler/objectivec/nsobject_methods.h" #include "google/protobuf/descriptor.pb.h" // NOTE: src/google/protobuf/compiler/plugin.cc makes use of cerr for some // error cases, so it seems to be ok to use as a back door for errors. namespace google { namespace protobuf { namespace compiler { namespace objectivec { namespace { bool BoolFromEnvVar(const char* env_var, bool default_value) { const char* value = getenv(env_var); if (value) { return std::string("YES") == absl::AsciiStrToUpper(value); } return default_value; } class SimpleLineCollector : public LineConsumer { public: explicit SimpleLineCollector(absl::flat_hash_set* inout_set) : set_(inout_set) {} bool ConsumeLine(absl::string_view line, std::string* out_error) override { set_->insert(std::string(line)); return true; } private: absl::flat_hash_set* set_; }; class PackageToPrefixesCollector : public LineConsumer { public: PackageToPrefixesCollector(absl::string_view usage, absl::flat_hash_map* inout_package_to_prefix_map) : usage_(usage), prefix_map_(inout_package_to_prefix_map) {} bool ConsumeLine(absl::string_view line, std::string* out_error) override; private: const std::string usage_; absl::flat_hash_map* prefix_map_; }; class PrefixModeStorage { public: PrefixModeStorage(); absl::string_view package_to_prefix_mappings_path() const { return package_to_prefix_mappings_path_; } void set_package_to_prefix_mappings_path(absl::string_view path) { package_to_prefix_mappings_path_ = std::string(path); package_to_prefix_map_.clear(); } absl::string_view prefix_from_proto_package_mappings( const FileDescriptor* file); bool use_package_name() const { return use_package_name_; } void set_use_package_name(bool on_or_off) { use_package_name_ = on_or_off; } absl::string_view exception_path() const { return exception_path_; } void set_exception_path(absl::string_view path) { exception_path_ = std::string(path); exceptions_.clear(); } bool is_package_exempted(absl::string_view package); // When using a proto package as the prefix, this should be added as the // prefix in front of it. absl::string_view forced_package_prefix() const { return forced_prefix_; } void set_forced_package_prefix(absl::string_view prefix) { forced_prefix_ = std::string(prefix); } private: bool use_package_name_; absl::flat_hash_map package_to_prefix_map_; std::string package_to_prefix_mappings_path_; std::string exception_path_; std::string forced_prefix_; absl::flat_hash_set exceptions_; }; PrefixModeStorage::PrefixModeStorage() { // Even thought there are generation options, have an env back door since some // of these helpers could be used in other plugins. use_package_name_ = BoolFromEnvVar("GPB_OBJC_USE_PACKAGE_AS_PREFIX", false); const char* exception_path = getenv("GPB_OBJC_PACKAGE_PREFIX_EXCEPTIONS_PATH"); if (exception_path) { exception_path_ = exception_path; } const char* prefix = getenv("GPB_OBJC_USE_PACKAGE_AS_PREFIX_PREFIX"); if (prefix) { forced_prefix_ = prefix; } } constexpr absl::string_view kNoPackagePrefix = "no_package:"; absl::string_view PrefixModeStorage::prefix_from_proto_package_mappings( const FileDescriptor* file) { if (!file) { return ""; } if (package_to_prefix_map_.empty() && !package_to_prefix_mappings_path_.empty()) { std::string error_str; // Re use the same collector as we use for expected_prefixes_path since the // file format is the same. PackageToPrefixesCollector collector("Package to prefixes", &package_to_prefix_map_); if (!ParseSimpleFile(package_to_prefix_mappings_path_, &collector, &error_str)) { if (error_str.empty()) { error_str = absl::StrCat("protoc:0: warning: Failed to parse ", "prefix to proto package mappings file: ", package_to_prefix_mappings_path_); } std::cerr << error_str << std::endl; std::cerr.flush(); package_to_prefix_map_.clear(); } } const std::string package = file->package(); // For files without packages, the can be registered as "no_package:PATH", // allowing the expected prefixes file. const std::string lookup_key = package.empty() ? absl::StrCat(kNoPackagePrefix, file->name()) : package; auto prefix_lookup = package_to_prefix_map_.find(lookup_key); if (prefix_lookup != package_to_prefix_map_.end()) { return prefix_lookup->second; } return ""; } bool PrefixModeStorage::is_package_exempted(absl::string_view package) { if (exceptions_.empty() && !exception_path_.empty()) { std::string error_str; SimpleLineCollector collector(&exceptions_); if (!ParseSimpleFile(exception_path_, &collector, &error_str)) { if (error_str.empty()) { error_str = std::string("protoc:0: warning: Failed to parse") + std::string(" package prefix exceptions file: ") + exception_path_; } std::cerr << error_str << std::endl; std::cerr.flush(); exceptions_.clear(); } // If the file was empty put something in it so it doesn't get reloaded over // and over. if (exceptions_.empty()) { exceptions_.insert(""); } } return exceptions_.contains(package); } PrefixModeStorage& g_prefix_mode = *new PrefixModeStorage(); } // namespace absl::string_view GetPackageToPrefixMappingsPath() { return g_prefix_mode.package_to_prefix_mappings_path(); } void SetPackageToPrefixMappingsPath(absl::string_view file_path) { g_prefix_mode.set_package_to_prefix_mappings_path(file_path); } bool UseProtoPackageAsDefaultPrefix() { return g_prefix_mode.use_package_name(); } void SetUseProtoPackageAsDefaultPrefix(bool on_or_off) { g_prefix_mode.set_use_package_name(on_or_off); } absl::string_view GetProtoPackagePrefixExceptionList() { return g_prefix_mode.exception_path(); } void SetProtoPackagePrefixExceptionList(absl::string_view file_path) { g_prefix_mode.set_exception_path(file_path); } absl::string_view GetForcedPackagePrefix() { return g_prefix_mode.forced_package_prefix(); } void SetForcedPackagePrefix(absl::string_view prefix) { g_prefix_mode.set_forced_package_prefix(prefix); } namespace { const char* const kUpperSegmentsList[] = {"url", "http", "https"}; const absl::flat_hash_set& UpperSegments() { static const auto* words = [] { auto* words = new absl::flat_hash_set(); for (const auto word : kUpperSegmentsList) { words->emplace(word); } return words; }(); return *words; } // Internal helper for name handing. // Do not expose this outside of helpers, stick to having functions for specific // cases (ClassName(), FieldName()), so there is always consistent suffix rules. std::string UnderscoresToCamelCase(absl::string_view input, bool first_capitalized) { std::vector values; std::string current; bool last_char_was_number = false; bool last_char_was_lower = false; bool last_char_was_upper = false; for (int i = 0; i < input.size(); i++) { char c = input[i]; if (absl::ascii_isdigit(c)) { if (!last_char_was_number) { values.push_back(current); current = ""; } current += c; last_char_was_number = last_char_was_lower = last_char_was_upper = false; last_char_was_number = true; } else if (absl::ascii_islower(c)) { // lowercase letter can follow a lowercase or uppercase letter if (!last_char_was_lower && !last_char_was_upper) { values.push_back(current); current = ""; } current += c; // already lower last_char_was_number = last_char_was_lower = last_char_was_upper = false; last_char_was_lower = true; } else if (absl::ascii_isupper(c)) { if (!last_char_was_upper) { values.push_back(current); current = ""; } current += absl::ascii_tolower(c); last_char_was_number = last_char_was_lower = last_char_was_upper = false; last_char_was_upper = true; } else { last_char_was_number = last_char_was_lower = last_char_was_upper = false; } } values.push_back(current); std::string result; bool first_segment_forces_upper = false; for (auto& value : values) { bool all_upper = UpperSegments().contains(value); if (all_upper && (result.length() == 0)) { first_segment_forces_upper = true; } if (all_upper) { absl::AsciiStrToUpper(&value); } else { value[0] = absl::ascii_toupper(value[0]); } result += value; } if ((result.length() != 0) && !first_capitalized && !first_segment_forces_upper) { result[0] = absl::ascii_tolower(result[0]); } return result; } const char* const kReservedWordList[] = { // Note NSObject Methods: // These are brought in from nsobject_methods.h that is generated // using method_dump.sh. See kNSObjectMethods below. // Objective-C "keywords" that aren't in C // From // http://stackoverflow.com/questions/1873630/reserved-keywords-in-objective-c // with some others added on. "id", "_cmd", "super", "in", "out", "inout", "bycopy", "byref", "oneway", "self", "instancetype", "nullable", "nonnull", "nil", "Nil", "YES", "NO", "weak", // C/C++ keywords (Incl C++ 0x11) // From http://en.cppreference.com/w/cpp/keywords "and", "and_eq", "alignas", "alignof", "asm", "auto", "bitand", "bitor", "bool", "break", "case", "catch", "char", "char16_t", "char32_t", "class", "compl", "const", "constexpr", "const_cast", "continue", "decltype", "default", "delete", "double", "dynamic_cast", "else", "enum", "explicit", "export", "extern ", "false", "float", "for", "friend", "goto", "if", "inline", "int", "long", "mutable", "namespace", "new", "noexcept", "not", "not_eq", "nullptr", "operator", "or", "or_eq", "private", "protected", "public", "register", "reinterpret_cast", "return", "short", "signed", "sizeof", "static", "static_assert", "static_cast", "struct", "switch", "template", "this", "thread_local", "throw", "true", "try", "typedef", "typeid", "typename", "union", "unsigned", "using", "virtual", "void", "volatile", "wchar_t", "while", "xor", "xor_eq", // C99 keywords // From // http://publib.boulder.ibm.com/infocenter/lnxpcomp/v8v101/index.jsp?topic=%2Fcom.ibm.xlcpp8l.doc%2Flanguage%2Fref%2Fkeyw.htm "restrict", // GCC/Clang extension "typeof", // Not a keyword, but will break you "NULL", // C88+ specs call for these to be macros, so depending on what they are // defined to be it can lead to odd errors for some Xcode/SDK versions. "stdin", "stdout", "stderr", // Objective-C Runtime typedefs // From "Category", "Ivar", "Method", "Protocol", // GPBMessage Methods // Only need to add instance methods that may conflict with // method declared in protos. The main cases are methods // that take no arguments, or setFoo:/hasFoo: type methods. "clear", "data", "delimitedData", "descriptor", "extensionRegistry", "extensionsCurrentlySet", "initialized", "isInitialized", "serializedSize", "sortedExtensionsInUse", "unknownFields", // MacTypes.h names "Fixed", "Fract", "Size", "LogicalAddress", "PhysicalAddress", "ByteCount", "ByteOffset", "Duration", "AbsoluteTime", "OptionBits", "ItemCount", "PBVersion", "ScriptCode", "LangCode", "RegionCode", "OSType", "ProcessSerialNumber", "Point", "Rect", "FixedPoint", "FixedRect", "Style", "StyleParameter", "StyleField", "TimeScale", "TimeBase", "TimeRecord", }; const absl::flat_hash_set& ReservedWords() { static const auto* words = [] { auto* words = new absl::flat_hash_set(); for (const auto word : kReservedWordList) { words->emplace(word); } return words; }(); return *words; } const absl::flat_hash_set& NSObjectMethods() { static const auto* words = [] { auto* words = new absl::flat_hash_set(); for (const auto word : kNSObjectMethodsList) { words->emplace(word); } return words; }(); return *words; } // returns true is input starts with __ or _[A-Z] which are reserved identifiers // in C/ C++. All calls should go through UnderscoresToCamelCase before getting // here but this verifies and allows for future expansion if we decide to // redefine what a reserved C identifier is (for example the GNU list // https://www.gnu.org/software/libc/manual/html_node/Reserved-Names.html ) bool IsReservedCIdentifier(absl::string_view input) { if (input.length() > 2) { if (input.at(0) == '_') { if (isupper(input.at(1)) || input.at(1) == '_') { return true; } } } return false; } std::string SanitizeNameForObjC(absl::string_view prefix, absl::string_view input, absl::string_view extension, std::string* out_suffix_added) { std::string sanitized; // We add the prefix in the cases where the string is missing a prefix. // We define "missing a prefix" as where 'input': // a) Doesn't start with the prefix or // b) Isn't equivalent to the prefix or // c) Has the prefix, but the letter after the prefix is lowercase if (absl::StartsWith(input, prefix)) { if (input.length() == prefix.length() || !absl::ascii_isupper(input[prefix.length()])) { sanitized = absl::StrCat(prefix, input); } else { sanitized = std::string(input); } } else { sanitized = absl::StrCat(prefix, input); } if (IsReservedCIdentifier(sanitized) || ReservedWords().contains(sanitized) || NSObjectMethods().contains(sanitized)) { if (out_suffix_added) *out_suffix_added = std::string(extension); return absl::StrCat(sanitized, extension); } if (out_suffix_added) out_suffix_added->clear(); return sanitized; } std::string NameFromFieldDescriptor(const FieldDescriptor* field) { if (field->type() == FieldDescriptor::TYPE_GROUP) { return field->message_type()->name(); } else { return field->name(); } } void PathSplit(absl::string_view path, std::string* directory, std::string* basename) { absl::string_view::size_type last_slash = path.rfind('/'); if (last_slash == absl::string_view::npos) { if (directory) { *directory = ""; } if (basename) { *basename = std::string(path); } } else { if (directory) { *directory = std::string(path.substr(0, last_slash)); } if (basename) { *basename = std::string(path.substr(last_slash + 1)); } } } bool IsSpecialNamePrefix(absl::string_view name, const std::vector& special_names) { for (const auto& special_name : special_names) { const size_t length = special_name.length(); if (name.compare(0, length, special_name) == 0) { if (name.length() > length) { // If name is longer than the special_name that it matches the next // character must be not lower case (newton vs newTon vs new_ton). return !absl::ascii_islower(name[length]); } else { return true; } } } return false; } void MaybeUnQuote(absl::string_view* input) { if ((input->length() >= 2) && ((*input->data() == '\'' || *input->data() == '"')) && ((*input)[input->length() - 1] == *input->data())) { input->remove_prefix(1); input->remove_suffix(1); } } } // namespace bool IsRetainedName(absl::string_view name) { // List of prefixes from // http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmRules.html static const std::vector* retained_names = new std::vector({"new", "alloc", "copy", "mutableCopy"}); return IsSpecialNamePrefix(name, *retained_names); } bool IsInitName(absl::string_view name) { static const std::vector* init_names = new std::vector({"init"}); return IsSpecialNamePrefix(name, *init_names); } bool IsCreateName(absl::string_view name) { // List of segments from // https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFMemoryMgmt/Concepts/Ownership.html#//apple_ref/doc/uid/20001148-103029 static const std::vector* create_names = new std::vector({"Create", "Copy"}); for (const auto& create_name : *create_names) { const size_t length = create_name.length(); size_t pos = name.find(create_name); if (pos != std::string::npos) { // The above docs don't actually call out anything about the characters // before the special words. So it's not clear if something like // "FOOCreate" would or would not match the "The Create Rule", but by not // checking, and claiming it does match, then callers will annotate with // `cf_returns_not_retained` which will ensure things work as desired. // // The footnote here is the docs do have a passing reference to "NoCopy", // but again, not looking for that and just returning `true` will cause // callers to annotate the api as not being a Create Rule function. // If name is longer than the create_names[i] that it matches the next // character must be not lower case (Copyright vs CopyFoo vs Copy_Foo). if (name.length() > pos + length) { return !absl::ascii_islower(name[pos + length]); } else { return true; } } } return false; } std::string BaseFileName(const FileDescriptor* file) { std::string basename; PathSplit(file->name(), nullptr, &basename); return basename; } std::string FileClassPrefix(const FileDescriptor* file) { // Always honor the file option. if (file->options().has_objc_class_prefix()) { return file->options().objc_class_prefix(); } // If package prefix is specified in an prefix to proto mappings file then use // that. absl::string_view objc_class_prefix = g_prefix_mode.prefix_from_proto_package_mappings(file); if (!objc_class_prefix.empty()) { return std::string(objc_class_prefix); } // If package prefix isn't enabled, done. if (!g_prefix_mode.use_package_name()) { return ""; } // If the package is in the exceptions list, done. if (g_prefix_mode.is_package_exempted(file->package())) { return ""; } // Transform the package into a prefix: use the dot segments as part, // camelcase each one and then join them with underscores, and add an // underscore at the end. std::string result; const std::vector segments = absl::StrSplit(file->package(), '.', absl::SkipEmpty()); for (const auto& segment : segments) { const std::string part = UnderscoresToCamelCase(segment, true); if (part.empty()) { continue; } if (!result.empty()) { result.append("_"); } result.append(part); } if (!result.empty()) { result.append("_"); } return absl::StrCat(g_prefix_mode.forced_package_prefix(), result); } std::string FilePath(const FileDescriptor* file) { std::string output; std::string basename; std::string directory; PathSplit(file->name(), &directory, &basename); if (directory.length() > 0) { output = absl::StrCat(directory, "/"); } basename = StripProto(basename); // CamelCase to be more ObjC friendly. basename = UnderscoresToCamelCase(basename, true); return absl::StrCat(output, basename); } std::string FilePathBasename(const FileDescriptor* file) { std::string output; std::string basename; std::string directory; PathSplit(file->name(), &directory, &basename); basename = StripProto(basename); // CamelCase to be more ObjC friendly. output = UnderscoresToCamelCase(basename, true); return output; } std::string FileClassName(const FileDescriptor* file) { const std::string prefix = FileClassPrefix(file); const std::string name = absl::StrCat( UnderscoresToCamelCase(StripProto(BaseFileName(file)), true), "Root"); // There aren't really any reserved words that end in "Root", but playing // it safe and checking. return SanitizeNameForObjC(prefix, name, "_RootClass", nullptr); } std::string ClassNameWorker(const Descriptor* descriptor) { std::string name; if (descriptor->containing_type() != nullptr) { return absl::StrCat(ClassNameWorker(descriptor->containing_type()), "_", descriptor->name()); } return absl::StrCat(name, descriptor->name()); } std::string ClassNameWorker(const EnumDescriptor* descriptor) { std::string name; if (descriptor->containing_type() != nullptr) { return absl::StrCat(ClassNameWorker(descriptor->containing_type()), "_", descriptor->name()); } return absl::StrCat(name, descriptor->name()); } std::string ClassName(const Descriptor* descriptor) { return ClassName(descriptor, nullptr); } std::string ClassName(const Descriptor* descriptor, std::string* out_suffix_added) { // 1. Message names are used as is (style calls for CamelCase, trust it). // 2. Check for reserved word at the very end and then suffix things. const std::string prefix = FileClassPrefix(descriptor->file()); const std::string name = ClassNameWorker(descriptor); return SanitizeNameForObjC(prefix, name, "_Class", out_suffix_added); } std::string EnumName(const EnumDescriptor* descriptor) { // 1. Enum names are used as is (style calls for CamelCase, trust it). // 2. Check for reserved word at the every end and then suffix things. // message Fixed { // message Size {...} // enum Mumble {...} // ... // } // yields Fixed_Class, Fixed_Size. const std::string prefix = FileClassPrefix(descriptor->file()); const std::string name = ClassNameWorker(descriptor); return SanitizeNameForObjC(prefix, name, "_Enum", nullptr); } std::string EnumValueName(const EnumValueDescriptor* descriptor) { // Because of the Switch enum compatibility, the name on the enum has to have // the suffix handing, so it slightly diverges from how nested classes work. // enum Fixed { // FOO = 1 // } // yields Fixed_Enum and Fixed_Enum_Foo (not Fixed_Foo). const std::string class_name = EnumName(descriptor->type()); const std::string value_str = UnderscoresToCamelCase(descriptor->name(), true); const std::string name = absl::StrCat(class_name, "_", value_str); // There aren't really any reserved words with an underscore and a leading // capital letter, but playing it safe and checking. return SanitizeNameForObjC("", name, "_Value", nullptr); } std::string EnumValueShortName(const EnumValueDescriptor* descriptor) { // Enum value names (EnumValueName above) are the enum name turned into // a class name and then the value name is CamelCased and concatenated; the // whole thing then gets sanitized for reserved words. // The "short name" is intended to be the final leaf, the value name; but // you can't simply send that off to sanitize as that could result in it // getting modified when the full name didn't. For example enum // "StorageModes" has a value "retain". So the full name is // "StorageModes_Retain", but if we sanitize "retain" it would become // "RetainValue". // So the right way to get the short name is to take the full enum name // and then strip off the enum name (leaving the value name and anything // done by sanitize). const std::string class_name = EnumName(descriptor->type()); const std::string long_name_prefix = absl::StrCat(class_name, "_"); const std::string long_name = EnumValueName(descriptor); return std::string(absl::StripPrefix(long_name, long_name_prefix)); } std::string UnCamelCaseEnumShortName(absl::string_view name) { std::string result; for (int i = 0; i < name.size(); i++) { char c = name[i]; if (i > 0 && absl::ascii_isupper(c)) { result += '_'; } result += absl::ascii_toupper(c); } return result; } std::string ExtensionMethodName(const FieldDescriptor* descriptor) { const std::string name = NameFromFieldDescriptor(descriptor); const std::string result = UnderscoresToCamelCase(name, false); return SanitizeNameForObjC("", result, "_Extension", nullptr); } std::string FieldName(const FieldDescriptor* field) { const std::string name = NameFromFieldDescriptor(field); std::string result = UnderscoresToCamelCase(name, false); if (field->is_repeated() && !field->is_map()) { // Add "Array" before do check for reserved worlds. absl::StrAppend(&result, "Array"); } else { // If it wasn't repeated, but ends in "Array", force on the _p suffix. if (absl::EndsWith(result, "Array")) { absl::StrAppend(&result, "_p"); } } return SanitizeNameForObjC("", result, "_p", nullptr); } std::string FieldNameCapitalized(const FieldDescriptor* field) { // Want the same suffix handling, so upcase the first letter of the other // name. std::string result = FieldName(field); if (result.length() > 0) { result[0] = absl::ascii_toupper(result[0]); } return result; } std::string OneofEnumName(const OneofDescriptor* descriptor) { const Descriptor* fieldDescriptor = descriptor->containing_type(); std::string name = absl::StrCat( ClassName(fieldDescriptor), "_", UnderscoresToCamelCase(descriptor->name(), true), "_OneOfCase"); // No sanitize needed because the OS never has names that end in _OneOfCase. return name; } std::string OneofName(const OneofDescriptor* descriptor) { std::string name = UnderscoresToCamelCase(descriptor->name(), false); // No sanitize needed because it gets OneOfCase added and that shouldn't // ever conflict. return name; } std::string OneofNameCapitalized(const OneofDescriptor* descriptor) { // Use the common handling and then up-case the first letter. std::string result = OneofName(descriptor); if (result.length() > 0) { result[0] = absl::ascii_toupper(result[0]); } return result; } std::string UnCamelCaseFieldName(absl::string_view name, const FieldDescriptor* field) { absl::string_view worker(name); if (absl::EndsWith(worker, "_p")) { worker = absl::StripSuffix(worker, "_p"); } if (field->is_repeated() && absl::EndsWith(worker, "Array")) { worker = absl::StripSuffix(worker, "Array"); } if (field->type() == FieldDescriptor::TYPE_GROUP) { if (worker.length() > 0) { if (absl::ascii_islower(worker[0])) { std::string copy(worker); copy[0] = absl::ascii_toupper(worker[0]); return copy; } } return std::string(worker); } else { std::string result; for (int i = 0; i < worker.size(); i++) { char c = worker[i]; if (absl::ascii_isupper(c)) { if (i > 0) { result += '_'; } result += absl::ascii_tolower(c); } else { result += c; } } return result; } } // Making these a generator option for folks that don't use CocoaPods, but do // want to put the library in a framework is an interesting question. The // problem is it means changing sources shipped with the library to actually // use a different value; so it isn't as simple as a option. const char* const ProtobufLibraryFrameworkName = "Protobuf"; std::string ProtobufFrameworkImportSymbol(absl::string_view framework_name) { // GPB_USE_[framework_name]_FRAMEWORK_IMPORTS return absl::StrCat("GPB_USE_", absl::AsciiStrToUpper(framework_name), "_FRAMEWORK_IMPORTS"); } bool IsProtobufLibraryBundledProtoFile(const FileDescriptor* file) { // We don't check the name prefix or proto package because some files // (descriptor.proto), aren't shipped generated by the library, so this // seems to be the safest way to only catch the ones shipped. const std::string name = file->name(); if (name == "google/protobuf/any.proto" || name == "google/protobuf/api.proto" || name == "google/protobuf/duration.proto" || name == "google/protobuf/empty.proto" || name == "google/protobuf/field_mask.proto" || name == "google/protobuf/source_context.proto" || name == "google/protobuf/struct.proto" || name == "google/protobuf/timestamp.proto" || name == "google/protobuf/type.proto" || name == "google/protobuf/wrappers.proto") { return true; } return false; } namespace { bool PackageToPrefixesCollector::ConsumeLine(absl::string_view line, std::string* out_error) { int offset = line.find('='); if (offset == absl::string_view::npos) { *out_error = absl::StrCat(usage_, " file line without equal sign: '", line, "'."); return false; } absl::string_view package = absl::StripAsciiWhitespace(line.substr(0, offset)); absl::string_view prefix = absl::StripAsciiWhitespace(line.substr(offset + 1)); MaybeUnQuote(&prefix); // Don't really worry about error checking the package/prefix for // being valid. Assume the file is validated when it is created/edited. (*prefix_map_)[package] = std::string(prefix); return true; } bool LoadExpectedPackagePrefixes( absl::string_view expected_prefixes_path, absl::flat_hash_map* prefix_map, std::string* out_error) { if (expected_prefixes_path.empty()) { return true; } PackageToPrefixesCollector collector("Expected prefixes", prefix_map); return ParseSimpleFile(expected_prefixes_path, &collector, out_error); } bool ValidateObjCClassPrefix( const FileDescriptor* file, absl::string_view expected_prefixes_path, const absl::flat_hash_map& expected_package_prefixes, bool prefixes_must_be_registered, bool require_prefixes, std::string* out_error) { // Reminder: An explicit prefix option of "" is valid in case the default // prefixing is set to use the proto package and a file needs to be generated // without any prefix at all (for legacy reasons). bool has_prefix = file->options().has_objc_class_prefix(); bool have_expected_prefix_file = !expected_prefixes_path.empty(); const std::string prefix = file->options().objc_class_prefix(); const std::string package = file->package(); // For files without packages, the can be registered as "no_package:PATH", // allowing the expected prefixes file. const std::string lookup_key = package.empty() ? absl::StrCat(kNoPackagePrefix, file->name()) : package; // NOTE: src/google/protobuf/compiler/plugin.cc makes use of cerr for some // error cases, so it seems to be ok to use as a back door for warnings. // Check: Error - See if there was an expected prefix for the package and // report if it doesn't match (wrong or missing). auto package_match = expected_package_prefixes.find(lookup_key); if (package_match != expected_package_prefixes.end()) { // There was an entry, and... if (has_prefix && package_match->second == prefix) { // ...it matches. All good, out of here! return true; } else { // ...it didn't match! *out_error = absl::StrCat("error: Expected 'option objc_class_prefix = \"", package_match->second, "\";'"); if (!package.empty()) { absl::StrAppend(out_error, " for package '", package, "'"); } absl::StrAppend(out_error, " in '", file->name(), "'"); if (has_prefix) { absl::StrAppend(out_error, "; but found '", prefix, "' instead"); } absl::StrAppend(out_error, "."); return false; } } // If there was no prefix option, we're done at this point. if (!has_prefix) { if (require_prefixes) { *out_error = absl::StrCat("error: '", file->name(), "' does not have a required 'option" " objc_class_prefix'."); return false; } return true; } // When the prefix is non empty, check it against the expected entries. if (!prefix.empty() && have_expected_prefix_file) { // For a non empty prefix, look for any other package that uses the prefix. std::string other_package_for_prefix; for (auto i = expected_package_prefixes.begin(); i != expected_package_prefixes.end(); ++i) { if (i->second == prefix) { other_package_for_prefix = i->first; // Stop on the first real package listing, if it was a no_package file // specific entry, keep looking to try and find a package one. if (!absl::StartsWith(other_package_for_prefix, kNoPackagePrefix)) { break; } } } // Check: Error - Make sure the prefix wasn't expected for a different // package (overlap is allowed, but it has to be listed as an expected // overlap). if (!other_package_for_prefix.empty()) { *out_error = absl::StrCat("error: Found 'option objc_class_prefix = \"", prefix, "\";' in '", file->name(), "'; that prefix is already used for "); if (absl::StartsWith(other_package_for_prefix, kNoPackagePrefix)) { absl::StrAppend( out_error, "file '", absl::StripPrefix(other_package_for_prefix, kNoPackagePrefix), "'."); } else { absl::StrAppend(out_error, "'package ", other_package_for_prefix, ";'."); } absl::StrAppend(out_error, " It can only be reused by adding '", lookup_key, " = ", prefix, "' to the expected prefixes file (", expected_prefixes_path, ")."); return false; // Only report first usage of the prefix. } } // !prefix.empty() && have_expected_prefix_file // Check: Warning - Make sure the prefix is is a reasonable value according // to Apple's rules (the checks above implicitly whitelist anything that // doesn't meet these rules). if (!prefix.empty() && !absl::ascii_isupper(prefix[0])) { std::cerr << "protoc:0: warning: Invalid 'option objc_class_prefix = \"" << prefix << "\";' in '" << file->name() << "';" << " it should start with a capital letter." << std::endl; std::cerr.flush(); } if (!prefix.empty() && prefix.length() < 3) { // Apple reserves 2 character prefixes for themselves. They do use some // 3 character prefixes, but they haven't updated the rules/docs. std::cerr << "protoc:0: warning: Invalid 'option objc_class_prefix = \"" << prefix << "\";' in '" << file->name() << "';" << " Apple recommends they should be at least 3 characters long." << std::endl; std::cerr.flush(); } // Check: Error/Warning - If the given package/prefix pair wasn't expected, // issue a error/warning to added to the file. if (have_expected_prefix_file) { if (prefixes_must_be_registered) { *out_error = absl::StrCat( "error: '", file->name(), "' has 'option objc_class_prefix = \"", prefix, "\";', but it is not registered. Add '", lookup_key, " = ", (prefix.empty() ? "\"\"" : prefix), "' to the expected prefixes file (", expected_prefixes_path, ")."); return false; } std::cerr << "protoc:0: warning: Found unexpected 'option objc_class_prefix = \"" << prefix << "\";' in '" << file->name() << "'; consider adding '" << lookup_key << " = " << (prefix.empty() ? "\"\"" : prefix) << "' to the expected prefixes file (" << expected_prefixes_path << ")." << std::endl; std::cerr.flush(); } return true; } } // namespace Options::Options() { // While there are generator options, also support env variables to help with // build systems where it isn't as easy to hook in for add the generation // options when invoking protoc. const char* file_path = getenv("GPB_OBJC_EXPECTED_PACKAGE_PREFIXES"); if (file_path) { expected_prefixes_path = file_path; } const char* suppressions = getenv("GPB_OBJC_EXPECTED_PACKAGE_PREFIXES_SUPPRESSIONS"); if (suppressions) { expected_prefixes_suppressions = absl::StrSplit(suppressions, ';', absl::SkipEmpty()); } prefixes_must_be_registered = BoolFromEnvVar("GPB_OBJC_PREFIXES_MUST_BE_REGISTERED", false); require_prefixes = BoolFromEnvVar("GPB_OBJC_REQUIRE_PREFIXES", false); } bool ValidateObjCClassPrefixes(const std::vector& files, std::string* out_error) { // Options's ctor load from the environment. Options options; return ValidateObjCClassPrefixes(files, options, out_error); } bool ValidateObjCClassPrefixes(const std::vector& files, const Options& validation_options, std::string* out_error) { // Allow a '-' as the path for the expected prefixes to completely disable // even the most basic of checks. if (validation_options.expected_prefixes_path == "-") { return true; } // Load the expected package prefixes, if available, to validate against. absl::flat_hash_map expected_package_prefixes; if (!LoadExpectedPackagePrefixes(validation_options.expected_prefixes_path, &expected_package_prefixes, out_error)) { return false; } for (auto file : files) { bool should_skip = (std::find(validation_options.expected_prefixes_suppressions.begin(), validation_options.expected_prefixes_suppressions.end(), file->name()) != validation_options.expected_prefixes_suppressions.end()); if (should_skip) { continue; } bool is_valid = ValidateObjCClassPrefix(file, validation_options.expected_prefixes_path, expected_package_prefixes, validation_options.prefixes_must_be_registered, validation_options.require_prefixes, out_error); if (!is_valid) { return false; } } return true; } } // namespace objectivec } // namespace compiler } // namespace protobuf } // namespace google