• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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