1 #region Copyright notice and license 2 // Protocol Buffers - Google's data interchange format 3 // Copyright 2008 Google Inc. All rights reserved. 4 // 5 // Use of this source code is governed by a BSD-style 6 // license that can be found in the LICENSE file or at 7 // https://developers.google.com/open-source/licenses/bsd 8 #endregion 9 10 using Google.Protobuf.Collections; 11 using Google.Protobuf.Compatibility; 12 using System; 13 14 namespace Google.Protobuf.Reflection 15 { 16 /// <summary> 17 /// Descriptor for a field or extension within a message in a .proto file. 18 /// </summary> 19 public sealed class FieldDescriptor : DescriptorBase, IComparable<FieldDescriptor> 20 { 21 private EnumDescriptor enumType; 22 private MessageDescriptor extendeeType; 23 private MessageDescriptor messageType; 24 private IFieldAccessor accessor; 25 26 /// <summary> 27 /// Get the field's containing message type, or <c>null</c> if it is a field defined at the top level of a file as an extension. 28 /// </summary> 29 public MessageDescriptor ContainingType { get; } 30 31 /// <summary> 32 /// Returns the oneof containing this field, or <c>null</c> if it is not part of a oneof. 33 /// </summary> 34 public OneofDescriptor ContainingOneof { get; } 35 36 /// <summary> 37 /// Returns the oneof containing this field if it's a "real" oneof, or <c>null</c> if either this 38 /// field is not part of a oneof, or the oneof is synthetic. 39 /// </summary> 40 public OneofDescriptor RealContainingOneof => ContainingOneof?.IsSynthetic == false ? ContainingOneof : null; 41 42 /// <summary> 43 /// The effective JSON name for this field. This is usually the lower-camel-cased form of the field name, 44 /// but can be overridden using the <c>json_name</c> option in the .proto file. 45 /// </summary> 46 public string JsonName { get; } 47 48 /// <summary> 49 /// The name of the property in the <c>ContainingType.ClrType</c> class. 50 /// </summary> 51 public string PropertyName { get; } 52 53 /// <summary> 54 /// Indicates whether this field supports presence, either implicitly (e.g. due to it being a message 55 /// type field) or explicitly via Has/Clear members. If this returns true, it is safe to call 56 /// <see cref="IFieldAccessor.Clear(IMessage)"/> and <see cref="IFieldAccessor.HasValue(IMessage)"/> 57 /// on this field's accessor with a suitable message. 58 /// </summary> 59 public bool HasPresence => 60 Extension != null ? !Extension.IsRepeated 61 : IsRepeated ? false 62 : IsMap ? false 63 : FieldType == FieldType.Message ? true 64 : FieldType == FieldType.Group ? true 65 // This covers "real oneof members" and "proto3 optional fields" 66 : ContainingOneof != null ? true 67 : Features.FieldPresence != FeatureSet.Types.FieldPresence.Implicit; 68 69 internal FieldDescriptorProto Proto { get; } 70 71 /// <summary> 72 /// Returns a clone of the underlying <see cref="FieldDescriptorProto"/> describing this field. 73 /// Note that a copy is taken every time this method is called, so clients using it frequently 74 /// (and not modifying it) may want to cache the returned value. 75 /// </summary> 76 /// <returns>A protobuf representation of this field descriptor.</returns> ToProto()77 public FieldDescriptorProto ToProto() => Proto.Clone(); 78 79 /// <summary> 80 /// An extension identifier for this field, or <c>null</c> if this field isn't an extension. 81 /// </summary> 82 public Extension Extension { get; } 83 FieldDescriptor(FieldDescriptorProto proto, FileDescriptor file, MessageDescriptor parent, int index, string propertyName, Extension extension)84 internal FieldDescriptor(FieldDescriptorProto proto, FileDescriptor file, 85 MessageDescriptor parent, int index, string propertyName, Extension extension) 86 : base(file, file.ComputeFullName(parent, proto.Name), index, 87 GetDirectParentFeatures(proto, file, parent).MergedWith(InferFeatures(file, proto)).MergedWith(proto.Options?.Features)) 88 { 89 Proto = proto; 90 if (proto.HasType) 91 { 92 FieldType = GetFieldTypeFromProtoType(proto.Type); 93 if (FieldType == FieldType.Message && 94 Features.MessageEncoding == FeatureSet.Types.MessageEncoding.Delimited) 95 { 96 FieldType = FieldType.Group; 97 } 98 } 99 100 if (FieldNumber <= 0) 101 { 102 throw new DescriptorValidationException(this, "Field numbers must be positive integers."); 103 } 104 ContainingType = parent; 105 if (proto.HasOneofIndex) 106 { 107 if (proto.OneofIndex < 0 || proto.OneofIndex >= parent.Proto.OneofDecl.Count) 108 { 109 throw new DescriptorValidationException(this, 110 $"FieldDescriptorProto.oneof_index is out of range for type {parent.Name}"); 111 } 112 ContainingOneof = parent.Oneofs[proto.OneofIndex]; 113 } 114 115 file.DescriptorPool.AddSymbol(this); 116 // We can't create the accessor until we've cross-linked, unfortunately, as we 117 // may not know whether the type of the field is a map or not. Remember the property name 118 // for later. 119 // We could trust the generated code and check whether the type of the property is 120 // a MapField, but that feels a tad nasty. 121 PropertyName = propertyName; 122 Extension = extension; 123 JsonName = Proto.JsonName == "" ? JsonFormatter.ToJsonName(Proto.Name) : Proto.JsonName; 124 } 125 126 /// <summary> 127 /// Returns the features from the direct parent: 128 /// - The file for top-level extensions 129 /// - The oneof for one-of fields 130 /// - Otherwise the message 131 /// </summary> GetDirectParentFeatures(FieldDescriptorProto proto, FileDescriptor file, MessageDescriptor parent)132 private static FeatureSetDescriptor GetDirectParentFeatures(FieldDescriptorProto proto, FileDescriptor file, MessageDescriptor parent) => 133 parent is null ? file.Features 134 // Ignore invalid oneof indexes here; they'll be validated later anyway. 135 : proto.OneofIndex >= 0 && proto.OneofIndex < parent.Proto.OneofDecl.Count ? parent.Oneofs[proto.OneofIndex].Features 136 : parent.Features; 137 138 /// <summary> 139 /// Returns a feature set with inferred features for the given field, or null if no features 140 /// need to be inferred. 141 /// </summary> 142 private static FeatureSet InferFeatures(FileDescriptor file, FieldDescriptorProto proto) 143 { 144 if ((int) file.Edition >= (int) Edition._2023) 145 { 146 return null; 147 } 148 // This is lazily initialized, as most fields won't need it. 149 FeatureSet features = null; 150 if (proto.Label == FieldDescriptorProto.Types.Label.Required) 151 { 152 features ??= new FeatureSet(); 153 features.FieldPresence = FeatureSet.Types.FieldPresence.LegacyRequired; 154 } 155 if (proto.Type == FieldDescriptorProto.Types.Type.Group) 156 { 157 features ??= new FeatureSet(); 158 features.MessageEncoding = FeatureSet.Types.MessageEncoding.Delimited; 159 } 160 if (file.Edition == Edition.Proto2 && (proto.Options?.Packed ?? false)) 161 { 162 features ??= new FeatureSet(); 163 features.RepeatedFieldEncoding = FeatureSet.Types.RepeatedFieldEncoding.Packed; 164 } 165 if (file.Edition == Edition.Proto3 && !(proto.Options?.Packed ?? true)) 166 { 167 features ??= new FeatureSet(); 168 features.RepeatedFieldEncoding = FeatureSet.Types.RepeatedFieldEncoding.Expanded; 169 } 170 return features; 171 } 172 173 /// <summary> 174 /// The brief name of the descriptor's target. 175 /// </summary> 176 public override string Name => Proto.Name; 177 178 /// <summary> 179 /// Returns the accessor for this field. 180 /// </summary> 181 /// <remarks> 182 /// <para> 183 /// While a <see cref="FieldDescriptor"/> describes the field, it does not provide 184 /// any way of obtaining or changing the value of the field within a specific message; 185 /// that is the responsibility of the accessor. 186 /// </para> 187 /// <para> 188 /// In descriptors for generated code, the value returned by this property will be non-null for all 189 /// regular fields. However, if a message containing a map field is introspected, the list of nested messages will include 190 /// an auto-generated nested key/value pair message for the field. This is not represented in any 191 /// generated type, and the value of the map field itself is represented by a dictionary in the 192 /// reflection API. There are never instances of those "hidden" messages, so no accessor is provided 193 /// and this property will return null. 194 /// </para> 195 /// <para> 196 /// In dynamically loaded descriptors, the value returned by this property will current be null; 197 /// if and when dynamic messages are supported, it will return a suitable accessor to work with 198 /// them. 199 /// </para> 200 /// </remarks> 201 public IFieldAccessor Accessor => accessor; 202 203 /// <summary> 204 /// Maps a field type as included in the .proto file to a FieldType. 205 /// </summary> GetFieldTypeFromProtoType(FieldDescriptorProto.Types.Type type)206 private static FieldType GetFieldTypeFromProtoType(FieldDescriptorProto.Types.Type type) 207 { 208 return type switch 209 { 210 FieldDescriptorProto.Types.Type.Double => FieldType.Double, 211 FieldDescriptorProto.Types.Type.Float => FieldType.Float, 212 FieldDescriptorProto.Types.Type.Int64 => FieldType.Int64, 213 FieldDescriptorProto.Types.Type.Uint64 => FieldType.UInt64, 214 FieldDescriptorProto.Types.Type.Int32 => FieldType.Int32, 215 FieldDescriptorProto.Types.Type.Fixed64 => FieldType.Fixed64, 216 FieldDescriptorProto.Types.Type.Fixed32 => FieldType.Fixed32, 217 FieldDescriptorProto.Types.Type.Bool => FieldType.Bool, 218 FieldDescriptorProto.Types.Type.String => FieldType.String, 219 FieldDescriptorProto.Types.Type.Group => FieldType.Group, 220 FieldDescriptorProto.Types.Type.Message => FieldType.Message, 221 FieldDescriptorProto.Types.Type.Bytes => FieldType.Bytes, 222 FieldDescriptorProto.Types.Type.Uint32 => FieldType.UInt32, 223 FieldDescriptorProto.Types.Type.Enum => FieldType.Enum, 224 FieldDescriptorProto.Types.Type.Sfixed32 => FieldType.SFixed32, 225 FieldDescriptorProto.Types.Type.Sfixed64 => FieldType.SFixed64, 226 FieldDescriptorProto.Types.Type.Sint32 => FieldType.SInt32, 227 FieldDescriptorProto.Types.Type.Sint64 => FieldType.SInt64, 228 _ => throw new ArgumentException("Invalid type specified"), 229 }; 230 } 231 232 /// <summary> 233 /// Returns <c>true</c> if this field is a repeated field; <c>false</c> otherwise. 234 /// </summary> 235 public bool IsRepeated => Proto.Label == FieldDescriptorProto.Types.Label.Repeated; 236 237 /// <summary> 238 /// Returns <c>true</c> if this field is a required field; <c>false</c> otherwise. 239 /// </summary> 240 public bool IsRequired => Features.FieldPresence == FeatureSet.Types.FieldPresence.LegacyRequired; 241 242 /// <summary> 243 /// Returns <c>true</c> if this field is a map field; <c>false</c> otherwise. 244 /// </summary> 245 public bool IsMap => FieldType == FieldType.Message && messageType.IsMapEntry; 246 247 /// <summary> 248 /// Returns <c>true</c> if this field is a packed, repeated field; <c>false</c> otherwise. 249 /// </summary> 250 public bool IsPacked => Features.RepeatedFieldEncoding == FeatureSet.Types.RepeatedFieldEncoding.Packed; 251 252 /// <summary> 253 /// Returns <c>true</c> if this field extends another message type; <c>false</c> otherwise. 254 /// </summary> 255 public bool IsExtension => Proto.HasExtendee; 256 257 /// <summary> 258 /// Returns the type of the field. 259 /// </summary> 260 public FieldType FieldType { get; private set; } 261 262 /// <summary> 263 /// Returns the field number declared in the proto file. 264 /// </summary> 265 public int FieldNumber => Proto.Number; 266 267 /// <summary> 268 /// Compares this descriptor with another one, ordering in "canonical" order 269 /// which simply means ascending order by field number. <paramref name="other"/> 270 /// must be a field of the same type, i.e. the <see cref="ContainingType"/> of 271 /// both fields must be the same. 272 /// </summary> CompareTo(FieldDescriptor other)273 public int CompareTo(FieldDescriptor other) 274 { 275 if (other.ContainingType != ContainingType) 276 { 277 throw new ArgumentException("FieldDescriptors can only be compared to other FieldDescriptors " + 278 "for fields of the same message type."); 279 } 280 return FieldNumber - other.FieldNumber; 281 } 282 283 /// <summary> 284 /// For enum fields, returns the field's type. 285 /// </summary> 286 public EnumDescriptor EnumType 287 { 288 get 289 { 290 if (FieldType != FieldType.Enum) 291 { 292 throw new InvalidOperationException("EnumType is only valid for enum fields."); 293 } 294 return enumType; 295 } 296 } 297 298 /// <summary> 299 /// For embedded message and group fields, returns the field's type. 300 /// </summary> 301 public MessageDescriptor MessageType 302 { 303 get 304 { 305 if (FieldType != FieldType.Message && FieldType != FieldType.Group) 306 { 307 throw new InvalidOperationException("MessageType is only valid for message or group fields."); 308 } 309 return messageType; 310 } 311 } 312 313 /// <summary> 314 /// For extension fields, returns the extended type 315 /// </summary> 316 public MessageDescriptor ExtendeeType 317 { 318 get 319 { 320 if (!Proto.HasExtendee) 321 { 322 throw new InvalidOperationException("ExtendeeType is only valid for extension fields."); 323 } 324 return extendeeType; 325 } 326 } 327 328 /// <summary> 329 /// The (possibly empty) set of custom options for this field. 330 /// </summary> 331 [Obsolete("CustomOptions are obsolete. Use the GetOptions() method.")] 332 public CustomOptions CustomOptions => new CustomOptions(Proto.Options?._extensions?.ValuesByNumber); 333 334 /// <summary> 335 /// The <c>FieldOptions</c>, defined in <c>descriptor.proto</c>. 336 /// If the options message is not present (i.e. there are no options), <c>null</c> is returned. 337 /// Custom options can be retrieved as extensions of the returned message. 338 /// NOTE: A defensive copy is created each time this property is retrieved. 339 /// </summary> GetOptions()340 public FieldOptions GetOptions() 341 { 342 var clone = Proto.Options?.Clone(); 343 if (clone is null) 344 { 345 return null; 346 } 347 // Clients should be using feature accessor methods, not accessing features on the 348 // options proto. 349 clone.Features = null; 350 return clone; 351 } 352 353 /// <summary> 354 /// Gets a single value field option for this descriptor 355 /// </summary> 356 [Obsolete("GetOption is obsolete. Use the GetOptions() method.")] GetOption(Extension<FieldOptions, T> extension)357 public T GetOption<T>(Extension<FieldOptions, T> extension) 358 { 359 var value = Proto.Options.GetExtension(extension); 360 return value is IDeepCloneable<T> ? (value as IDeepCloneable<T>).Clone() : value; 361 } 362 363 /// <summary> 364 /// Gets a repeated value field option for this descriptor 365 /// </summary> 366 [Obsolete("GetOption is obsolete. Use the GetOptions() method.")] GetOption(RepeatedExtension<FieldOptions, T> extension)367 public RepeatedField<T> GetOption<T>(RepeatedExtension<FieldOptions, T> extension) 368 { 369 return Proto.Options.GetExtension(extension).Clone(); 370 } 371 372 /// <summary> 373 /// Look up and cross-link all field types etc. 374 /// </summary> CrossLink()375 internal void CrossLink() 376 { 377 if (Proto.HasTypeName) 378 { 379 IDescriptor typeDescriptor = 380 File.DescriptorPool.LookupSymbol(Proto.TypeName, this); 381 382 // In most cases, the type will be specified in the descriptor proto. This may be 383 // guaranteed in descriptor.proto in the future (with respect to spring 2024), but 384 // we may still see older descriptors created by old versions of protoc, and there 385 // may be some code creating descriptor protos directly. This code effectively 386 // maintains backward compatibility, but we don't expect it to be a path taken 387 // often at all. 388 if (!Proto.HasType) 389 { 390 // Choose field type based on symbol. 391 if (typeDescriptor is MessageDescriptor) 392 { 393 FieldType = 394 Features.MessageEncoding == FeatureSet.Types.MessageEncoding.Delimited 395 ? FieldType.Group 396 : FieldType.Message; 397 } 398 else if (typeDescriptor is EnumDescriptor) 399 { 400 FieldType = FieldType.Enum; 401 } 402 else 403 { 404 throw new DescriptorValidationException(this, $"\"{Proto.TypeName}\" is not a type."); 405 } 406 } 407 408 if (FieldType == FieldType.Message || FieldType == FieldType.Group) 409 { 410 if (typeDescriptor is not MessageDescriptor m) 411 { 412 throw new DescriptorValidationException(this, $"\"{Proto.TypeName}\" is not a message type."); 413 } 414 messageType = m; 415 if (m.Proto.Options?.MapEntry == true || ContainingType?.Proto.Options?.MapEntry == true) 416 { 417 // Maps can't inherit delimited encoding. 418 FieldType = FieldType.Message; 419 } 420 421 if (Proto.HasDefaultValue) 422 { 423 throw new DescriptorValidationException(this, "Messages can't have default values."); 424 } 425 } 426 else if (FieldType == FieldType.Enum) 427 { 428 if (typeDescriptor is not EnumDescriptor e) 429 { 430 throw new DescriptorValidationException(this, $"\"{Proto.TypeName}\" is not an enum type."); 431 } 432 enumType = e; 433 } 434 else 435 { 436 throw new DescriptorValidationException(this, "Field with primitive type has type_name."); 437 } 438 } 439 else 440 { 441 if (FieldType == FieldType.Message || FieldType == FieldType.Enum) 442 { 443 throw new DescriptorValidationException(this, "Field with message or enum type missing type_name."); 444 } 445 } 446 447 if (Proto.HasExtendee) 448 { 449 extendeeType = File.DescriptorPool.LookupSymbol(Proto.Extendee, this) as MessageDescriptor; 450 } 451 452 // Note: no attempt to perform any default value parsing 453 454 File.DescriptorPool.AddFieldByNumber(this); 455 456 if (ContainingType != null && ContainingType.Proto.Options != null && ContainingType.Proto.Options.MessageSetWireFormat) 457 { 458 throw new DescriptorValidationException(this, "MessageSet format is not supported."); 459 } 460 accessor = CreateAccessor(); 461 } 462 CreateAccessor()463 private IFieldAccessor CreateAccessor() 464 { 465 if (Extension != null) 466 { 467 return new ExtensionAccessor(this); 468 } 469 470 // If we're given no property name, that's because we really don't want an accessor. 471 // This could be because it's a map message, or it could be that we're loading a FileDescriptor dynamically. 472 // TODO: Support dynamic messages. 473 if (PropertyName == null) 474 { 475 return null; 476 } 477 478 var property = ContainingType.ClrType.GetProperty(PropertyName); 479 if (property == null) 480 { 481 throw new DescriptorValidationException(this, $"Property {PropertyName} not found in {ContainingType.ClrType}"); 482 } 483 return IsMap ? new MapFieldAccessor(property, this) 484 : IsRepeated ? new RepeatedFieldAccessor(property, this) 485 : (IFieldAccessor) new SingleFieldAccessor(ContainingType.ClrType, property, this); 486 } 487 } 488 } 489