blob: 0b55005c82a84c81754253f633a21787551e53a8 [file] [log] [blame]
#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.Concurrent;
using System.Collections.Generic;
using System.Linq;
using static Google.Protobuf.Reflection.FeatureSet.Types;
namespace Google.Protobuf.Reflection;
/// <summary>
/// A resolved set of features for a file, message etc.
/// </summary>
/// <remarks>
/// Only features supported by the C# runtime are exposed; currently
/// all enums in C# are open, and we never perform UTF-8 validation.
/// If either of those features are ever implemented in this runtime,
/// the feature settings will be exposed as properties in this class.
/// </remarks>
internal sealed partial class FeatureSetDescriptor
{
private static readonly ConcurrentDictionary<FeatureSet, FeatureSetDescriptor> cache = new();
private static readonly IReadOnlyDictionary<Edition, FeatureSetDescriptor> descriptorsByEdition = BuildEditionDefaults();
// Note: if the debugger is set to break within this code, various type initializers will fail
// as the debugger will try to call ToString() on messages, requiring descriptors to be accessed etc.
// There's a possible workaround of using a hard-coded bootstrapping FeatureSetDescriptor to be returned
// by GetEditionDefaults if descriptorsByEdition is null, but it's ugly and likely just pushes the problem
// elsewhere. Normal debugging sessions (where the initial bootstrapping code doesn't hit any breakpoints)
// do not cause any problems.
private static IReadOnlyDictionary<Edition, FeatureSetDescriptor> BuildEditionDefaults()
{
var featureSetDefaults = FeatureSetDefaults.Parser.ParseFrom(Convert.FromBase64String(DefaultsBase64));
var ret = new Dictionary<Edition, FeatureSetDescriptor>();
// Note: Enum.GetValues<TEnum> isn't available until .NET 5. It's not worth making this conditional
// based on that.
var supportedEditions = ((Edition[]) Enum.GetValues(typeof(Edition)))
.OrderBy(x => x)
.Where(e => e >= featureSetDefaults.MinimumEdition && e <= featureSetDefaults.MaximumEdition);
// We assume the embedded defaults will always contain "legacy".
var currentDescriptor = MaybeCreateDescriptor(Edition.Legacy);
foreach (var edition in supportedEditions)
{
currentDescriptor = MaybeCreateDescriptor(edition) ?? currentDescriptor;
ret[edition] = currentDescriptor;
}
return ret;
FeatureSetDescriptor MaybeCreateDescriptor(Edition edition)
{
var editionDefaults = featureSetDefaults.Defaults.SingleOrDefault(d => d.Edition == edition);
if (editionDefaults is null)
{
return null;
}
var proto = new FeatureSet();
proto.MergeFrom(editionDefaults.FixedFeatures);
proto.MergeFrom(editionDefaults.OverridableFeatures);
return new FeatureSetDescriptor(proto);
}
}
internal static FeatureSetDescriptor GetEditionDefaults(Edition edition) =>
descriptorsByEdition.TryGetValue(edition, out var defaults) ? defaults
: throw new ArgumentOutOfRangeException($"Unsupported edition: {edition}");
// Visible for testing. The underlying feature set proto, usually derived during
// feature resolution.
internal FeatureSet Proto { get; }
/// <summary>
/// Only relevant to fields. Indicates if a field has explicit presence.
/// </summary>
internal FieldPresence FieldPresence => Proto.FieldPresence;
/// <summary>
/// Only relevant to fields. Indicates how a repeated field should be encoded.
/// </summary>
internal RepeatedFieldEncoding RepeatedFieldEncoding => Proto.RepeatedFieldEncoding;
/// <summary>
/// Only relevant to fields. Indicates how a message-valued field should be encoded.
/// </summary>
internal MessageEncoding MessageEncoding => Proto.MessageEncoding;
private FeatureSetDescriptor(FeatureSet proto)
{
Proto = proto;
}
/// <summary>
/// Returns a new descriptor based on this one, with the specified overrides.
/// Multiple calls to this method that produce equivalent feature sets will return
/// the same instance.
/// </summary>
/// <param name="overrides">The proto representation of the "child" feature set to merge with this
/// one. May be null, in which case this descriptor is returned.</param>
/// <returns>A descriptor based on the current one, with the given set of overrides.</returns>
public FeatureSetDescriptor MergedWith(FeatureSet overrides)
{
if (overrides is null)
{
return this;
}
// Note: It would be nice if we could avoid cloning unless
// there are actual changes, but this won't happen that often;
// it'll be temporary garbage.
var clone = Proto.Clone();
clone.MergeFrom(overrides);
return cache.GetOrAdd(clone, clone => new FeatureSetDescriptor(clone));
}
}