#region Copyright notice and license // Protocol Buffers - Google's data interchange format // Copyright 2008 Google Inc. All rights reserved. // // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd #endregion using System; using System.Collections.Generic; using System.Text; namespace Google.Protobuf.Reflection { /// /// Contains lookup tables containing all the descriptors defined in a particular file. /// internal sealed class DescriptorPool { private readonly IDictionary descriptorsByName = new Dictionary(); private readonly IDictionary, FieldDescriptor> fieldsByNumber = new Dictionary, FieldDescriptor>(); private readonly IDictionary, EnumValueDescriptor> enumValuesByNumber = new Dictionary, EnumValueDescriptor>(); private readonly IDictionary enumValuesByName = new Dictionary(); private readonly HashSet dependencies = new HashSet(); internal DescriptorPool(IEnumerable dependencyFiles) { foreach (FileDescriptor dependencyFile in dependencyFiles) { dependencies.Add(dependencyFile); ImportPublicDependencies(dependencyFile); } foreach (FileDescriptor dependency in dependencyFiles) { AddPackage(dependency.Package, dependency); } } private void ImportPublicDependencies(FileDescriptor file) { foreach (FileDescriptor dependency in file.PublicDependencies) { if (dependencies.Add(dependency)) { ImportPublicDependencies(dependency); } } } /// /// Finds a symbol of the given name within the pool. /// /// The type of symbol to look for /// Fully-qualified name to look up /// The symbol with the given name and type, /// or null if the symbol doesn't exist or has the wrong type internal T FindSymbol(string fullName) where T : class { descriptorsByName.TryGetValue(fullName, out IDescriptor result); if (result is T descriptor) { return descriptor; } // dependencies contains direct dependencies and any *public* dependencies // of those dependencies (transitively)... so we don't need to recurse here. foreach (FileDescriptor dependency in dependencies) { dependency.DescriptorPool.descriptorsByName.TryGetValue(fullName, out result); descriptor = result as T; if (descriptor != null) { return descriptor; } } return null; } /// /// Adds a package to the symbol tables. If a package by the same name /// already exists, that is fine, but if some other kind of symbol /// exists under the same name, an exception is thrown. If the package /// has multiple components, this also adds the parent package(s). /// internal void AddPackage(string fullName, FileDescriptor file) { int dotpos = fullName.LastIndexOf('.'); String name; if (dotpos != -1) { AddPackage(fullName.Substring(0, dotpos), file); name = fullName.Substring(dotpos + 1); } else { name = fullName; } if (descriptorsByName.TryGetValue(fullName, out IDescriptor old)) { if (old is not PackageDescriptor) { throw new DescriptorValidationException(file, "\"" + name + "\" is already defined (as something other than a " + "package) in file \"" + old.File.Name + "\"."); } } descriptorsByName[fullName] = new PackageDescriptor(name, fullName, file); } /// /// Adds a symbol to the symbol table. /// /// The symbol already existed /// in the symbol table. internal void AddSymbol(IDescriptor descriptor) { ValidateSymbolName(descriptor); string fullName = descriptor.FullName; if (descriptorsByName.TryGetValue(fullName, out IDescriptor old)) { throw new DescriptorValidationException(descriptor, GetDescriptorAlreadyAddedExceptionMessage(descriptor, fullName, old)); } descriptorsByName[fullName] = descriptor; } private static string GetDescriptorAlreadyAddedExceptionMessage(IDescriptor descriptor, string fullName, IDescriptor old) { int dotPos = fullName.LastIndexOf('.'); return descriptor.File != old.File ? $"\"{fullName}\" is already defined in file \"{old.File.Name}\"." : dotPos == -1 ? $"{fullName} is already defined." : $"\"{fullName.Substring(dotPos + 1)}\" is already defined in \"{fullName.Substring(0, dotPos)}\"."; } /// /// Verifies that the descriptor's name is valid (i.e. it contains /// only letters, digits and underscores, and does not start with a digit). /// /// private static void ValidateSymbolName(IDescriptor descriptor) { if (descriptor.Name.Length == 0) { throw new DescriptorValidationException(descriptor, "Missing name."); } // Symbol name must start with a letter or underscore, and it can contain letters, // numbers and underscores. string name = descriptor.Name; if (!IsAsciiLetter(name[0]) && name[0] != '_') { ThrowInvalidSymbolNameException(descriptor); } for (int i = 1; i < name.Length; i++) { if (!IsAsciiLetter(name[i]) && !IsAsciiDigit(name[i]) && name[i] != '_') { ThrowInvalidSymbolNameException(descriptor); } } static bool IsAsciiLetter(char c) => (uint)((c | 0x20) - 'a') <= 'z' - 'a'; static bool IsAsciiDigit(char c) => (uint)(c - '0') <= '9' - '0'; static void ThrowInvalidSymbolNameException(IDescriptor descriptor) => throw new DescriptorValidationException( descriptor, "\"" + descriptor.Name + "\" is not a valid identifier."); } /// /// Returns the field with the given number in the given descriptor, /// or null if it can't be found. /// internal FieldDescriptor FindFieldByNumber(MessageDescriptor messageDescriptor, int number) { fieldsByNumber.TryGetValue(new ObjectIntPair(messageDescriptor, number), out FieldDescriptor ret); return ret; } internal EnumValueDescriptor FindEnumValueByNumber(EnumDescriptor enumDescriptor, int number) { enumValuesByNumber.TryGetValue(new ObjectIntPair(enumDescriptor, number), out EnumValueDescriptor ret); return ret; } internal EnumValueDescriptor FindEnumValueByName(EnumDescriptor enumDescriptor, string name) { enumValuesByName.TryGetValue(new EnumValueByNameDescriptorKey(enumDescriptor, name), out EnumValueDescriptor ret); return ret; } /// /// Adds a field to the fieldsByNumber table. /// /// A field with the same /// containing type and number already exists. internal void AddFieldByNumber(FieldDescriptor field) { // for extensions, we use the extended type, otherwise we use the containing type ObjectIntPair key = new ObjectIntPair(field.Proto.HasExtendee ? field.ExtendeeType : field.ContainingType, field.FieldNumber); if (fieldsByNumber.TryGetValue(key, out FieldDescriptor old)) { throw new DescriptorValidationException(field, "Field number " + field.FieldNumber + "has already been used in \"" + field.ContainingType.FullName + "\" by field \"" + old.Name + "\"."); } fieldsByNumber[key] = field; } /// /// Adds an enum value to the enumValuesByNumber and enumValuesByName tables. If an enum value /// with the same type and number already exists, this method does nothing to enumValuesByNumber. /// (This is allowed; the first value defined with the number takes precedence.) If an enum /// value with the same name already exists, this method throws DescriptorValidationException. /// (It is expected that this method is called after AddSymbol, which would already have thrown /// an exception in this failure case.) /// internal void AddEnumValue(EnumValueDescriptor enumValue) { ObjectIntPair numberKey = new ObjectIntPair(enumValue.EnumDescriptor, enumValue.Number); if (!enumValuesByNumber.ContainsKey(numberKey)) { enumValuesByNumber[numberKey] = enumValue; } EnumValueByNameDescriptorKey nameKey = new EnumValueByNameDescriptorKey(enumValue.EnumDescriptor, enumValue.Name); if (enumValuesByName.TryGetValue(nameKey, out EnumValueDescriptor old)) { throw new DescriptorValidationException(enumValue, GetDescriptorAlreadyAddedExceptionMessage(enumValue, enumValue.FullName, old)); } enumValuesByName[nameKey] = enumValue; } /// /// Looks up a descriptor by name, relative to some other descriptor. /// The name may be fully-qualified (with a leading '.'), partially-qualified, /// or unqualified. C++-like name lookup semantics are used to search for the /// matching descriptor. /// /// /// This isn't heavily optimized, but it's only used during cross linking anyway. /// If it starts being used more widely, we should look at performance more carefully. /// internal IDescriptor LookupSymbol(string name, IDescriptor relativeTo) { IDescriptor result; if (name.StartsWith(".")) { // Fully-qualified name. result = FindSymbol(name.Substring(1)); } else { // If "name" is a compound identifier, we want to search for the // first component of it, then search within it for the rest. int firstPartLength = name.IndexOf('.'); string firstPart = firstPartLength == -1 ? name : name.Substring(0, firstPartLength); // We will search each parent scope of "relativeTo" looking for the // symbol. StringBuilder scopeToTry = new StringBuilder(relativeTo.FullName); while (true) { // Chop off the last component of the scope. int dotpos = scopeToTry.ToString().LastIndexOf("."); if (dotpos == -1) { result = FindSymbol(name); break; } else { scopeToTry.Length = dotpos + 1; // Append firstPart and try to find. scopeToTry.Append(firstPart); result = FindSymbol(scopeToTry.ToString()); if (result != null) { if (firstPartLength != -1) { // We only found the first part of the symbol. Now look for // the whole thing. If this fails, we *don't* want to keep // searching parent scopes. scopeToTry.Length = dotpos + 1; scopeToTry.Append(name); result = FindSymbol(scopeToTry.ToString()); } break; } // Not found. Remove the name so we can try again. scopeToTry.Length = dotpos; } } } if (result == null) { throw new DescriptorValidationException(relativeTo, "\"" + name + "\" is not defined."); } else { return result; } } /// /// Struct used to hold the keys for the enumValuesByName table. /// private struct EnumValueByNameDescriptorKey : IEquatable { private readonly string name; private readonly IDescriptor descriptor; internal EnumValueByNameDescriptorKey(EnumDescriptor descriptor, string valueName) { this.descriptor = descriptor; this.name = valueName; } public bool Equals(EnumValueByNameDescriptorKey other) => descriptor == other.descriptor && name == other.name; public override bool Equals(object obj) => obj is EnumValueByNameDescriptorKey pair && Equals(pair); public override int GetHashCode() { unchecked { var hashCode = descriptor.GetHashCode(); hashCode = (hashCode * 397) ^ (name != null ? name.GetHashCode() : 0); return hashCode; } } } } }