1 #region Copyright notice and license 2 // Protocol Buffers - Google's data interchange format 3 // Copyright 2008 Google Inc. All rights reserved. 4 // https://developers.google.com/protocol-buffers/ 5 // 6 // Redistribution and use in source and binary forms, with or without 7 // modification, are permitted provided that the following conditions are 8 // met: 9 // 10 // * Redistributions of source code must retain the above copyright 11 // notice, this list of conditions and the following disclaimer. 12 // * Redistributions in binary form must reproduce the above 13 // copyright notice, this list of conditions and the following disclaimer 14 // in the documentation and/or other materials provided with the 15 // distribution. 16 // * Neither the name of Google Inc. nor the names of its 17 // contributors may be used to endorse or promote products derived from 18 // this software without specific prior written permission. 19 // 20 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 #endregion 32 33 using Google.Protobuf.Collections; 34 using Google.Protobuf.Compatibility; 35 using System; 36 37 namespace Google.Protobuf.Reflection 38 { 39 /// <summary> 40 /// Descriptor for a field or extension within a message in a .proto file. 41 /// </summary> 42 public sealed class FieldDescriptor : DescriptorBase, IComparable<FieldDescriptor> 43 { 44 private EnumDescriptor enumType; 45 private MessageDescriptor extendeeType; 46 private MessageDescriptor messageType; 47 private FieldType fieldType; 48 private readonly string propertyName; // Annoyingly, needed in Crosslink. 49 private IFieldAccessor accessor; 50 51 /// <summary> 52 /// 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. 53 /// </summary> 54 public MessageDescriptor ContainingType { get; } 55 56 /// <summary> 57 /// Returns the oneof containing this field, or <c>null</c> if it is not part of a oneof. 58 /// </summary> 59 public OneofDescriptor ContainingOneof { get; } 60 61 /// <summary> 62 /// Returns the oneof containing this field if it's a "real" oneof, or <c>null</c> if either this 63 /// field is not part of a oneof, or the oneof is synthetic. 64 /// </summary> 65 public OneofDescriptor RealContainingOneof => ContainingOneof?.IsSynthetic == false ? ContainingOneof : null; 66 67 /// <summary> 68 /// The effective JSON name for this field. This is usually the lower-camel-cased form of the field name, 69 /// but can be overridden using the <c>json_name</c> option in the .proto file. 70 /// </summary> 71 public string JsonName { get; } 72 73 /// <summary> 74 /// Indicates whether this field supports presence, either implicitly (e.g. due to it being a message 75 /// type field) or explicitly via Has/Clear members. If this returns true, it is safe to call 76 /// <see cref="IFieldAccessor.Clear(IMessage)"/> and <see cref="IFieldAccessor.HasValue(IMessage)"/> 77 /// on this field's accessor with a suitable message. 78 /// </summary> 79 public bool HasPresence => 80 Extension != null ? !Extension.IsRepeated 81 : IsRepeated ? false 82 : IsMap ? false 83 : FieldType == FieldType.Message ? true 84 // This covers "real oneof members" and "proto3 optional fields" 85 : ContainingOneof != null ? true 86 : File.Syntax == Syntax.Proto2; 87 88 internal FieldDescriptorProto Proto { get; } 89 90 /// <summary> 91 /// An extension identifier for this field, or <c>null</c> if this field isn't an extension. 92 /// </summary> 93 public Extension Extension { get; } 94 FieldDescriptor(FieldDescriptorProto proto, FileDescriptor file, MessageDescriptor parent, int index, string propertyName, Extension extension)95 internal FieldDescriptor(FieldDescriptorProto proto, FileDescriptor file, 96 MessageDescriptor parent, int index, string propertyName, Extension extension) 97 : base(file, file.ComputeFullName(parent, proto.Name), index) 98 { 99 Proto = proto; 100 if (proto.Type != 0) 101 { 102 fieldType = GetFieldTypeFromProtoType(proto.Type); 103 } 104 105 if (FieldNumber <= 0) 106 { 107 throw new DescriptorValidationException(this, "Field numbers must be positive integers."); 108 } 109 ContainingType = parent; 110 if (proto.HasOneofIndex) 111 { 112 if (proto.OneofIndex < 0 || proto.OneofIndex >= parent.Proto.OneofDecl.Count) 113 { 114 throw new DescriptorValidationException(this, 115 $"FieldDescriptorProto.oneof_index is out of range for type {parent.Name}"); 116 } 117 ContainingOneof = parent.Oneofs[proto.OneofIndex]; 118 } 119 120 file.DescriptorPool.AddSymbol(this); 121 // We can't create the accessor until we've cross-linked, unfortunately, as we 122 // may not know whether the type of the field is a map or not. Remember the property name 123 // for later. 124 // We could trust the generated code and check whether the type of the property is 125 // a MapField, but that feels a tad nasty. 126 this.propertyName = propertyName; 127 Extension = extension; 128 JsonName = Proto.JsonName == "" ? JsonFormatter.ToJsonName(Proto.Name) : Proto.JsonName; 129 } 130 131 132 /// <summary> 133 /// The brief name of the descriptor's target. 134 /// </summary> 135 public override string Name => Proto.Name; 136 137 /// <summary> 138 /// Returns the accessor for this field. 139 /// </summary> 140 /// <remarks> 141 /// <para> 142 /// While a <see cref="FieldDescriptor"/> describes the field, it does not provide 143 /// any way of obtaining or changing the value of the field within a specific message; 144 /// that is the responsibility of the accessor. 145 /// </para> 146 /// <para> 147 /// In descriptors for generated code, the value returned by this property will be non-null for all 148 /// regular fields. However, if a message containing a map field is introspected, the list of nested messages will include 149 /// an auto-generated nested key/value pair message for the field. This is not represented in any 150 /// generated type, and the value of the map field itself is represented by a dictionary in the 151 /// reflection API. There are never instances of those "hidden" messages, so no accessor is provided 152 /// and this property will return null. 153 /// </para> 154 /// <para> 155 /// In dynamically loaded descriptors, the value returned by this property will current be null; 156 /// if and when dynamic messages are supported, it will return a suitable accessor to work with 157 /// them. 158 /// </para> 159 /// </remarks> 160 public IFieldAccessor Accessor => accessor; 161 162 /// <summary> 163 /// Maps a field type as included in the .proto file to a FieldType. 164 /// </summary> GetFieldTypeFromProtoType(FieldDescriptorProto.Types.Type type)165 private static FieldType GetFieldTypeFromProtoType(FieldDescriptorProto.Types.Type type) 166 { 167 switch (type) 168 { 169 case FieldDescriptorProto.Types.Type.Double: 170 return FieldType.Double; 171 case FieldDescriptorProto.Types.Type.Float: 172 return FieldType.Float; 173 case FieldDescriptorProto.Types.Type.Int64: 174 return FieldType.Int64; 175 case FieldDescriptorProto.Types.Type.Uint64: 176 return FieldType.UInt64; 177 case FieldDescriptorProto.Types.Type.Int32: 178 return FieldType.Int32; 179 case FieldDescriptorProto.Types.Type.Fixed64: 180 return FieldType.Fixed64; 181 case FieldDescriptorProto.Types.Type.Fixed32: 182 return FieldType.Fixed32; 183 case FieldDescriptorProto.Types.Type.Bool: 184 return FieldType.Bool; 185 case FieldDescriptorProto.Types.Type.String: 186 return FieldType.String; 187 case FieldDescriptorProto.Types.Type.Group: 188 return FieldType.Group; 189 case FieldDescriptorProto.Types.Type.Message: 190 return FieldType.Message; 191 case FieldDescriptorProto.Types.Type.Bytes: 192 return FieldType.Bytes; 193 case FieldDescriptorProto.Types.Type.Uint32: 194 return FieldType.UInt32; 195 case FieldDescriptorProto.Types.Type.Enum: 196 return FieldType.Enum; 197 case FieldDescriptorProto.Types.Type.Sfixed32: 198 return FieldType.SFixed32; 199 case FieldDescriptorProto.Types.Type.Sfixed64: 200 return FieldType.SFixed64; 201 case FieldDescriptorProto.Types.Type.Sint32: 202 return FieldType.SInt32; 203 case FieldDescriptorProto.Types.Type.Sint64: 204 return FieldType.SInt64; 205 default: 206 throw new ArgumentException("Invalid type specified"); 207 } 208 } 209 210 /// <summary> 211 /// Returns <c>true</c> if this field is a repeated field; <c>false</c> otherwise. 212 /// </summary> 213 public bool IsRepeated => Proto.Label == FieldDescriptorProto.Types.Label.Repeated; 214 215 /// <summary> 216 /// Returns <c>true</c> if this field is a required field; <c>false</c> otherwise. 217 /// </summary> 218 public bool IsRequired => Proto.Label == FieldDescriptorProto.Types.Label.Required; 219 220 /// <summary> 221 /// Returns <c>true</c> if this field is a map field; <c>false</c> otherwise. 222 /// </summary> 223 public bool IsMap => fieldType == FieldType.Message && messageType.Proto.Options != null && messageType.Proto.Options.MapEntry; 224 225 /// <summary> 226 /// Returns <c>true</c> if this field is a packed, repeated field; <c>false</c> otherwise. 227 /// </summary> 228 public bool IsPacked 229 { 230 get 231 { 232 if (File.Syntax != Syntax.Proto3) 233 { 234 return Proto.Options?.Packed ?? false; 235 } 236 else 237 { 238 return !Proto.Options.HasPacked || Proto.Options.Packed; 239 } 240 } 241 } 242 243 /// <summary> 244 /// Returns <c>true</c> if this field extends another message type; <c>false</c> otherwise. 245 /// </summary> 246 public bool IsExtension => Proto.HasExtendee; 247 248 /// <summary> 249 /// Returns the type of the field. 250 /// </summary> 251 public FieldType FieldType => fieldType; 252 253 /// <summary> 254 /// Returns the field number declared in the proto file. 255 /// </summary> 256 public int FieldNumber => Proto.Number; 257 258 /// <summary> 259 /// Compares this descriptor with another one, ordering in "canonical" order 260 /// which simply means ascending order by field number. <paramref name="other"/> 261 /// must be a field of the same type, i.e. the <see cref="ContainingType"/> of 262 /// both fields must be the same. 263 /// </summary> CompareTo(FieldDescriptor other)264 public int CompareTo(FieldDescriptor other) 265 { 266 if (other.ContainingType != ContainingType) 267 { 268 throw new ArgumentException("FieldDescriptors can only be compared to other FieldDescriptors " + 269 "for fields of the same message type."); 270 } 271 return FieldNumber - other.FieldNumber; 272 } 273 274 /// <summary> 275 /// For enum fields, returns the field's type. 276 /// </summary> 277 public EnumDescriptor EnumType 278 { 279 get 280 { 281 if (fieldType != FieldType.Enum) 282 { 283 throw new InvalidOperationException("EnumType is only valid for enum fields."); 284 } 285 return enumType; 286 } 287 } 288 289 /// <summary> 290 /// For embedded message and group fields, returns the field's type. 291 /// </summary> 292 public MessageDescriptor MessageType 293 { 294 get 295 { 296 if (fieldType != FieldType.Message && fieldType != FieldType.Group) 297 { 298 throw new InvalidOperationException("MessageType is only valid for message or group fields."); 299 } 300 return messageType; 301 } 302 } 303 304 /// <summary> 305 /// For extension fields, returns the extended type 306 /// </summary> 307 public MessageDescriptor ExtendeeType 308 { 309 get 310 { 311 if (!Proto.HasExtendee) 312 { 313 throw new InvalidOperationException("ExtendeeType is only valid for extension fields."); 314 } 315 return extendeeType; 316 } 317 } 318 319 /// <summary> 320 /// The (possibly empty) set of custom options for this field. 321 /// </summary> 322 [Obsolete("CustomOptions are obsolete. Use the GetOptions() method.")] 323 public CustomOptions CustomOptions => new CustomOptions(Proto.Options?._extensions?.ValuesByNumber); 324 325 /// <summary> 326 /// The <c>FieldOptions</c>, defined in <c>descriptor.proto</c>. 327 /// If the options message is not present (i.e. there are no options), <c>null</c> is returned. 328 /// Custom options can be retrieved as extensions of the returned message. 329 /// NOTE: A defensive copy is created each time this property is retrieved. 330 /// </summary> GetOptions()331 public FieldOptions GetOptions() => Proto.Options?.Clone(); 332 333 /// <summary> 334 /// Gets a single value field option for this descriptor 335 /// </summary> 336 [Obsolete("GetOption is obsolete. Use the GetOptions() method.")] GetOption(Extension<FieldOptions, T> extension)337 public T GetOption<T>(Extension<FieldOptions, T> extension) 338 { 339 var value = Proto.Options.GetExtension(extension); 340 return value is IDeepCloneable<T> ? (value as IDeepCloneable<T>).Clone() : value; 341 } 342 343 /// <summary> 344 /// Gets a repeated value field option for this descriptor 345 /// </summary> 346 [Obsolete("GetOption is obsolete. Use the GetOptions() method.")] GetOption(RepeatedExtension<FieldOptions, T> extension)347 public RepeatedField<T> GetOption<T>(RepeatedExtension<FieldOptions, T> extension) 348 { 349 return Proto.Options.GetExtension(extension).Clone(); 350 } 351 352 /// <summary> 353 /// Look up and cross-link all field types etc. 354 /// </summary> CrossLink()355 internal void CrossLink() 356 { 357 if (Proto.HasTypeName) 358 { 359 IDescriptor typeDescriptor = 360 File.DescriptorPool.LookupSymbol(Proto.TypeName, this); 361 362 if (Proto.HasType) 363 { 364 // Choose field type based on symbol. 365 if (typeDescriptor is MessageDescriptor) 366 { 367 fieldType = FieldType.Message; 368 } 369 else if (typeDescriptor is EnumDescriptor) 370 { 371 fieldType = FieldType.Enum; 372 } 373 else 374 { 375 throw new DescriptorValidationException(this, $"\"{Proto.TypeName}\" is not a type."); 376 } 377 } 378 379 if (fieldType == FieldType.Message || fieldType == FieldType.Group) 380 { 381 if (!(typeDescriptor is MessageDescriptor)) 382 { 383 throw new DescriptorValidationException(this, $"\"{Proto.TypeName}\" is not a message type."); 384 } 385 messageType = (MessageDescriptor) typeDescriptor; 386 387 if (Proto.HasDefaultValue) 388 { 389 throw new DescriptorValidationException(this, "Messages can't have default values."); 390 } 391 } 392 else if (fieldType == FieldType.Enum) 393 { 394 if (!(typeDescriptor is EnumDescriptor)) 395 { 396 throw new DescriptorValidationException(this, $"\"{Proto.TypeName}\" is not an enum type."); 397 } 398 enumType = (EnumDescriptor) typeDescriptor; 399 } 400 else 401 { 402 throw new DescriptorValidationException(this, "Field with primitive type has type_name."); 403 } 404 } 405 else 406 { 407 if (fieldType == FieldType.Message || fieldType == FieldType.Enum) 408 { 409 throw new DescriptorValidationException(this, "Field with message or enum type missing type_name."); 410 } 411 } 412 413 if (Proto.HasExtendee) 414 { 415 extendeeType = File.DescriptorPool.LookupSymbol(Proto.Extendee, this) as MessageDescriptor; 416 } 417 418 // Note: no attempt to perform any default value parsing 419 420 File.DescriptorPool.AddFieldByNumber(this); 421 422 if (ContainingType != null && ContainingType.Proto.Options != null && ContainingType.Proto.Options.MessageSetWireFormat) 423 { 424 throw new DescriptorValidationException(this, "MessageSet format is not supported."); 425 } 426 accessor = CreateAccessor(); 427 } 428 CreateAccessor()429 private IFieldAccessor CreateAccessor() 430 { 431 if (Extension != null) 432 { 433 return new ExtensionAccessor(this); 434 } 435 436 // If we're given no property name, that's because we really don't want an accessor. 437 // This could be because it's a map message, or it could be that we're loading a FileDescriptor dynamically. 438 // TODO: Support dynamic messages. 439 if (propertyName == null) 440 { 441 return null; 442 } 443 444 var property = ContainingType.ClrType.GetProperty(propertyName); 445 if (property == null) 446 { 447 throw new DescriptorValidationException(this, $"Property {propertyName} not found in {ContainingType.ClrType}"); 448 } 449 return IsMap ? new MapFieldAccessor(property, this) 450 : IsRepeated ? new RepeatedFieldAccessor(property, this) 451 : (IFieldAccessor) new SingleFieldAccessor(property, this); 452 } 453 } 454 } 455