#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 Google.Protobuf.Collections; using Google.Protobuf.Compatibility; using System; namespace Google.Protobuf.Reflection { /// /// Descriptor for a field or extension within a message in a .proto file. /// public sealed class FieldDescriptor : DescriptorBase, IComparable { private EnumDescriptor enumType; private MessageDescriptor extendeeType; private MessageDescriptor messageType; private IFieldAccessor accessor; /// /// Get the field's containing message type, or null if it is a field defined at the top level of a file as an extension. /// public MessageDescriptor ContainingType { get; } /// /// Returns the oneof containing this field, or null if it is not part of a oneof. /// public OneofDescriptor ContainingOneof { get; } /// /// Returns the oneof containing this field if it's a "real" oneof, or null if either this /// field is not part of a oneof, or the oneof is synthetic. /// public OneofDescriptor RealContainingOneof => ContainingOneof?.IsSynthetic == false ? ContainingOneof : null; /// /// The effective JSON name for this field. This is usually the lower-camel-cased form of the field name, /// but can be overridden using the json_name option in the .proto file. /// public string JsonName { get; } /// /// The name of the property in the ContainingType.ClrType class. /// public string PropertyName { get; } /// /// Indicates whether this field supports presence, either implicitly (e.g. due to it being a message /// type field) or explicitly via Has/Clear members. If this returns true, it is safe to call /// and /// on this field's accessor with a suitable message. /// public bool HasPresence => Extension != null ? !Extension.IsRepeated : IsRepeated ? false : IsMap ? false : FieldType == FieldType.Message ? true : FieldType == FieldType.Group ? true // This covers "real oneof members" and "proto3 optional fields" : ContainingOneof != null ? true : Features.FieldPresence != FeatureSet.Types.FieldPresence.Implicit; internal FieldDescriptorProto Proto { get; } /// /// Returns a clone of the underlying describing this field. /// Note that a copy is taken every time this method is called, so clients using it frequently /// (and not modifying it) may want to cache the returned value. /// /// A protobuf representation of this field descriptor. public FieldDescriptorProto ToProto() => Proto.Clone(); /// /// An extension identifier for this field, or null if this field isn't an extension. /// public Extension Extension { get; } internal FieldDescriptor(FieldDescriptorProto proto, FileDescriptor file, MessageDescriptor parent, int index, string propertyName, Extension extension) : base(file, file.ComputeFullName(parent, proto.Name), index, GetDirectParentFeatures(proto, file, parent).MergedWith(InferFeatures(file, proto)).MergedWith(proto.Options?.Features)) { Proto = proto; if (proto.HasType) { FieldType = GetFieldTypeFromProtoType(proto.Type); if (FieldType == FieldType.Message && Features.MessageEncoding == FeatureSet.Types.MessageEncoding.Delimited) { FieldType = FieldType.Group; } } if (FieldNumber <= 0) { throw new DescriptorValidationException(this, "Field numbers must be positive integers."); } ContainingType = parent; if (proto.HasOneofIndex) { if (proto.OneofIndex < 0 || proto.OneofIndex >= parent.Proto.OneofDecl.Count) { throw new DescriptorValidationException(this, $"FieldDescriptorProto.oneof_index is out of range for type {parent.Name}"); } ContainingOneof = parent.Oneofs[proto.OneofIndex]; } file.DescriptorPool.AddSymbol(this); // We can't create the accessor until we've cross-linked, unfortunately, as we // may not know whether the type of the field is a map or not. Remember the property name // for later. // We could trust the generated code and check whether the type of the property is // a MapField, but that feels a tad nasty. PropertyName = propertyName; Extension = extension; JsonName = Proto.JsonName == "" ? JsonFormatter.ToJsonName(Proto.Name) : Proto.JsonName; } /// /// Returns the features from the direct parent: /// - The file for top-level extensions /// - The oneof for one-of fields /// - Otherwise the message /// private static FeatureSetDescriptor GetDirectParentFeatures(FieldDescriptorProto proto, FileDescriptor file, MessageDescriptor parent) => parent is null ? file.Features // Ignore invalid oneof indexes here; they'll be validated later anyway. : proto.OneofIndex >= 0 && proto.OneofIndex < parent.Proto.OneofDecl.Count ? parent.Oneofs[proto.OneofIndex].Features : parent.Features; /// /// Returns a feature set with inferred features for the given field, or null if no features /// need to be inferred. /// private static FeatureSet InferFeatures(FileDescriptor file, FieldDescriptorProto proto) { if ((int) file.Edition >= (int) Edition._2023) { return null; } // This is lazily initialized, as most fields won't need it. FeatureSet features = null; if (proto.Label == FieldDescriptorProto.Types.Label.Required) { features ??= new FeatureSet(); features.FieldPresence = FeatureSet.Types.FieldPresence.LegacyRequired; } if (proto.Type == FieldDescriptorProto.Types.Type.Group) { features ??= new FeatureSet(); features.MessageEncoding = FeatureSet.Types.MessageEncoding.Delimited; } if (file.Edition == Edition.Proto2 && (proto.Options?.Packed ?? false)) { features ??= new FeatureSet(); features.RepeatedFieldEncoding = FeatureSet.Types.RepeatedFieldEncoding.Packed; } if (file.Edition == Edition.Proto3 && !(proto.Options?.Packed ?? true)) { features ??= new FeatureSet(); features.RepeatedFieldEncoding = FeatureSet.Types.RepeatedFieldEncoding.Expanded; } return features; } /// /// The brief name of the descriptor's target. /// public override string Name => Proto.Name; /// /// Returns the accessor for this field. /// /// /// /// While a describes the field, it does not provide /// any way of obtaining or changing the value of the field within a specific message; /// that is the responsibility of the accessor. /// /// /// In descriptors for generated code, the value returned by this property will be non-null for all /// regular fields. However, if a message containing a map field is introspected, the list of nested messages will include /// an auto-generated nested key/value pair message for the field. This is not represented in any /// generated type, and the value of the map field itself is represented by a dictionary in the /// reflection API. There are never instances of those "hidden" messages, so no accessor is provided /// and this property will return null. /// /// /// In dynamically loaded descriptors, the value returned by this property will current be null; /// if and when dynamic messages are supported, it will return a suitable accessor to work with /// them. /// /// public IFieldAccessor Accessor => accessor; /// /// Maps a field type as included in the .proto file to a FieldType. /// private static FieldType GetFieldTypeFromProtoType(FieldDescriptorProto.Types.Type type) { return type switch { FieldDescriptorProto.Types.Type.Double => FieldType.Double, FieldDescriptorProto.Types.Type.Float => FieldType.Float, FieldDescriptorProto.Types.Type.Int64 => FieldType.Int64, FieldDescriptorProto.Types.Type.Uint64 => FieldType.UInt64, FieldDescriptorProto.Types.Type.Int32 => FieldType.Int32, FieldDescriptorProto.Types.Type.Fixed64 => FieldType.Fixed64, FieldDescriptorProto.Types.Type.Fixed32 => FieldType.Fixed32, FieldDescriptorProto.Types.Type.Bool => FieldType.Bool, FieldDescriptorProto.Types.Type.String => FieldType.String, FieldDescriptorProto.Types.Type.Group => FieldType.Group, FieldDescriptorProto.Types.Type.Message => FieldType.Message, FieldDescriptorProto.Types.Type.Bytes => FieldType.Bytes, FieldDescriptorProto.Types.Type.Uint32 => FieldType.UInt32, FieldDescriptorProto.Types.Type.Enum => FieldType.Enum, FieldDescriptorProto.Types.Type.Sfixed32 => FieldType.SFixed32, FieldDescriptorProto.Types.Type.Sfixed64 => FieldType.SFixed64, FieldDescriptorProto.Types.Type.Sint32 => FieldType.SInt32, FieldDescriptorProto.Types.Type.Sint64 => FieldType.SInt64, _ => throw new ArgumentException("Invalid type specified"), }; } /// /// Returns true if this field is a repeated field; false otherwise. /// public bool IsRepeated => Proto.Label == FieldDescriptorProto.Types.Label.Repeated; /// /// Returns true if this field is a required field; false otherwise. /// public bool IsRequired => Features.FieldPresence == FeatureSet.Types.FieldPresence.LegacyRequired; /// /// Returns true if this field is a map field; false otherwise. /// public bool IsMap => FieldType == FieldType.Message && messageType.IsMapEntry; /// /// Returns true if this field is a packed, repeated field; false otherwise. /// public bool IsPacked => Features.RepeatedFieldEncoding == FeatureSet.Types.RepeatedFieldEncoding.Packed; /// /// Returns true if this field extends another message type; false otherwise. /// public bool IsExtension => Proto.HasExtendee; /// /// Returns the type of the field. /// public FieldType FieldType { get; private set; } /// /// Returns the field number declared in the proto file. /// public int FieldNumber => Proto.Number; /// /// Compares this descriptor with another one, ordering in "canonical" order /// which simply means ascending order by field number. /// must be a field of the same type, i.e. the of /// both fields must be the same. /// public int CompareTo(FieldDescriptor other) { if (other.ContainingType != ContainingType) { throw new ArgumentException("FieldDescriptors can only be compared to other FieldDescriptors " + "for fields of the same message type."); } return FieldNumber - other.FieldNumber; } /// /// For enum fields, returns the field's type. /// public EnumDescriptor EnumType { get { if (FieldType != FieldType.Enum) { throw new InvalidOperationException("EnumType is only valid for enum fields."); } return enumType; } } /// /// For embedded message and group fields, returns the field's type. /// public MessageDescriptor MessageType { get { if (FieldType != FieldType.Message && FieldType != FieldType.Group) { throw new InvalidOperationException("MessageType is only valid for message or group fields."); } return messageType; } } /// /// For extension fields, returns the extended type /// public MessageDescriptor ExtendeeType { get { if (!Proto.HasExtendee) { throw new InvalidOperationException("ExtendeeType is only valid for extension fields."); } return extendeeType; } } /// /// The (possibly empty) set of custom options for this field. /// [Obsolete("CustomOptions are obsolete. Use the GetOptions() method.")] public CustomOptions CustomOptions => new CustomOptions(Proto.Options?._extensions?.ValuesByNumber); /// /// The FieldOptions, defined in descriptor.proto. /// If the options message is not present (i.e. there are no options), null is returned. /// Custom options can be retrieved as extensions of the returned message. /// NOTE: A defensive copy is created each time this property is retrieved. /// public FieldOptions GetOptions() { var clone = Proto.Options?.Clone(); if (clone is null) { return null; } // Clients should be using feature accessor methods, not accessing features on the // options proto. clone.Features = null; return clone; } /// /// Gets a single value field option for this descriptor /// [Obsolete("GetOption is obsolete. Use the GetOptions() method.")] public T GetOption(Extension extension) { var value = Proto.Options.GetExtension(extension); return value is IDeepCloneable ? (value as IDeepCloneable).Clone() : value; } /// /// Gets a repeated value field option for this descriptor /// [Obsolete("GetOption is obsolete. Use the GetOptions() method.")] public RepeatedField GetOption(RepeatedExtension extension) { return Proto.Options.GetExtension(extension).Clone(); } /// /// Look up and cross-link all field types etc. /// internal void CrossLink() { if (Proto.HasTypeName) { IDescriptor typeDescriptor = File.DescriptorPool.LookupSymbol(Proto.TypeName, this); // In most cases, the type will be specified in the descriptor proto. This may be // guaranteed in descriptor.proto in the future (with respect to spring 2024), but // we may still see older descriptors created by old versions of protoc, and there // may be some code creating descriptor protos directly. This code effectively // maintains backward compatibility, but we don't expect it to be a path taken // often at all. if (!Proto.HasType) { // Choose field type based on symbol. if (typeDescriptor is MessageDescriptor) { FieldType = Features.MessageEncoding == FeatureSet.Types.MessageEncoding.Delimited ? FieldType.Group : FieldType.Message; } else if (typeDescriptor is EnumDescriptor) { FieldType = FieldType.Enum; } else { throw new DescriptorValidationException(this, $"\"{Proto.TypeName}\" is not a type."); } } if (FieldType == FieldType.Message || FieldType == FieldType.Group) { if (typeDescriptor is not MessageDescriptor m) { throw new DescriptorValidationException(this, $"\"{Proto.TypeName}\" is not a message type."); } messageType = m; if (m.Proto.Options?.MapEntry == true || ContainingType?.Proto.Options?.MapEntry == true) { // Maps can't inherit delimited encoding. FieldType = FieldType.Message; } if (Proto.HasDefaultValue) { throw new DescriptorValidationException(this, "Messages can't have default values."); } } else if (FieldType == FieldType.Enum) { if (typeDescriptor is not EnumDescriptor e) { throw new DescriptorValidationException(this, $"\"{Proto.TypeName}\" is not an enum type."); } enumType = e; } else { throw new DescriptorValidationException(this, "Field with primitive type has type_name."); } } else { if (FieldType == FieldType.Message || FieldType == FieldType.Enum) { throw new DescriptorValidationException(this, "Field with message or enum type missing type_name."); } } if (Proto.HasExtendee) { extendeeType = File.DescriptorPool.LookupSymbol(Proto.Extendee, this) as MessageDescriptor; } // Note: no attempt to perform any default value parsing File.DescriptorPool.AddFieldByNumber(this); if (ContainingType != null && ContainingType.Proto.Options != null && ContainingType.Proto.Options.MessageSetWireFormat) { throw new DescriptorValidationException(this, "MessageSet format is not supported."); } accessor = CreateAccessor(); } private IFieldAccessor CreateAccessor() { if (Extension != null) { return new ExtensionAccessor(this); } // If we're given no property name, that's because we really don't want an accessor. // This could be because it's a map message, or it could be that we're loading a FileDescriptor dynamically. // TODO: Support dynamic messages. if (PropertyName == null) { return null; } var property = ContainingType.ClrType.GetProperty(PropertyName); if (property == null) { throw new DescriptorValidationException(this, $"Property {PropertyName} not found in {ContainingType.ClrType}"); } return IsMap ? new MapFieldAccessor(property, this) : IsRepeated ? new RepeatedFieldAccessor(property, this) : (IFieldAccessor) new SingleFieldAccessor(ContainingType.ClrType, property, this); } } }