// Copyright 2016-2019, Pulumi Corporation using System; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using Google.Protobuf.Collections; using Google.Protobuf.WellKnownTypes; namespace Pulumi.Serialization { internal static class Deserializer { private static OutputData DeserializeCore(Value value, Func> func) { var (innerVal, isSecret) = UnwrapSecret(value); value = innerVal; if (value.KindCase == Value.KindOneofCase.StringValue && value.StringValue == Constants.UnknownValue) { // always deserialize unknown as the null value. return new OutputData(ImmutableHashSet.Empty, default!, isKnown: false, isSecret); } if (TryDeserializeAssetOrArchive(value, out var assetOrArchive)) { return new OutputData(ImmutableHashSet.Empty, (T)(object)assetOrArchive, isKnown: true, isSecret); } if (TryDeserializeResource(value, out var resource)) { return new OutputData(ImmutableHashSet.Empty, (T)(object)resource, isKnown: true, isSecret); } var innerData = func(value); return OutputData.Create(innerData.Resources, innerData.Value, innerData.IsKnown, isSecret || innerData.IsSecret); } private static OutputData DeserializeOneOf(Value value, Value.KindOneofCase kind, Func> func) => DeserializeCore(value, v => v.KindCase == kind ? func(v) : throw new InvalidOperationException($"Trying to deserialize {v.KindCase} as a {kind}")); private static OutputData DeserializePrimitive(Value value, Value.KindOneofCase kind, Func func) => DeserializeOneOf(value, kind, v => OutputData.Create( ImmutableHashSet.Empty, func(v), isKnown: true, isSecret: false)); private static OutputData DeserializeBoolean(Value value) => DeserializePrimitive(value, Value.KindOneofCase.BoolValue, v => v.BoolValue); private static OutputData DeserializerString(Value value) => DeserializePrimitive(value, Value.KindOneofCase.StringValue, v => v.StringValue); private static OutputData DeserializerDouble(Value value) => DeserializePrimitive(value, Value.KindOneofCase.NumberValue, v => v.NumberValue); private static OutputData> DeserializeList(Value value) => DeserializeOneOf(value, Value.KindOneofCase.ListValue, v => { var resources = ImmutableHashSet.CreateBuilder(); var result = ImmutableArray.CreateBuilder(); var isKnown = true; var isSecret = false; foreach (var element in v.ListValue.Values) { var elementData = Deserialize(element); (isKnown, isSecret) = OutputData.Combine(elementData, isKnown, isSecret); resources.UnionWith(elementData.Resources); result.Add(elementData.Value); } return OutputData.Create(resources.ToImmutable(), result.ToImmutable(), isKnown, isSecret); }); private static OutputData> DeserializeStruct(Value value) => DeserializeOneOf(value, Value.KindOneofCase.StructValue, v => { var resources = ImmutableHashSet.CreateBuilder(); var result = ImmutableDictionary.CreateBuilder(); var isKnown = true; var isSecret = false; foreach (var (key, element) in v.StructValue.Fields) { // Unilaterally skip properties considered internal by the Pulumi engine. // These don't actually contribute to the exposed shape of the object, do // not need to be passed back to the engine, and often will not match the // expected type we are deserializing into. if (key.StartsWith("__", StringComparison.Ordinal)) continue; var elementData = Deserialize(element); (isKnown, isSecret) = OutputData.Combine(elementData, isKnown, isSecret); result.Add(key, elementData.Value); resources.UnionWith(elementData.Resources); } return OutputData.Create(resources.ToImmutable(), result.ToImmutable(), isKnown, isSecret); }); public static OutputData Deserialize(Value value) => DeserializeCore(value, v => v.KindCase switch { Value.KindOneofCase.NumberValue => DeserializerDouble(v), Value.KindOneofCase.StringValue => DeserializerString(v), Value.KindOneofCase.BoolValue => DeserializeBoolean(v), Value.KindOneofCase.StructValue => DeserializeStruct(v), Value.KindOneofCase.ListValue => DeserializeList(v), Value.KindOneofCase.NullValue => new OutputData(ImmutableHashSet.Empty, null, isKnown: true, isSecret: false), Value.KindOneofCase.None => throw new InvalidOperationException("Should never get 'None' type when deserializing protobuf"), _ => throw new InvalidOperationException("Unknown type when deserializing protobuf: " + v.KindCase), }); private static (Value unwrapped, bool isSecret) UnwrapSecret(Value value) { var isSecret = false; while (IsSpecialStruct(value, out var sig) && sig == Constants.SpecialSecretSig) { if (!value.StructValue.Fields.TryGetValue(Constants.SecretValueName, out var secretValue)) throw new InvalidOperationException("Secrets must have a field called 'value'"); isSecret = true; value = secretValue; } return (value, isSecret); } private static bool IsSpecialStruct( Value value, [NotNullWhen(true)] out string? sig) { if (value.KindCase == Value.KindOneofCase.StructValue && value.StructValue.Fields.TryGetValue(Constants.SpecialSigKey, out var sigVal) && sigVal.KindCase == Value.KindOneofCase.StringValue) { sig = sigVal.StringValue; return true; } sig = null; return false; } private static bool TryDeserializeAssetOrArchive( Value value, [NotNullWhen(true)] out AssetOrArchive? assetOrArchive) { if (IsSpecialStruct(value, out var sig)) { if (sig == Constants.SpecialAssetSig) { assetOrArchive = DeserializeAsset(value); return true; } if (sig == Constants.SpecialArchiveSig) { assetOrArchive = DeserializeArchive(value); return true; } } assetOrArchive = null; return false; } private static Archive DeserializeArchive(Value value) { if (TryGetStringValue(value.StructValue.Fields, Constants.AssetOrArchivePathName, out var path)) return new FileArchive(path); if (TryGetStringValue(value.StructValue.Fields, Constants.AssetOrArchiveUriName, out var uri)) return new RemoteArchive(uri); if (value.StructValue.Fields.TryGetValue(Constants.ArchiveAssetsName, out var assetsValue)) { if (assetsValue.KindCase == Value.KindOneofCase.StructValue) { var assets = ImmutableDictionary.CreateBuilder(); foreach (var (name, val) in assetsValue.StructValue.Fields) { if (!TryDeserializeAssetOrArchive(val, out var innerAssetOrArchive)) throw new InvalidOperationException("AssetArchive contained an element that wasn't itself an Asset or Archive."); assets[name] = innerAssetOrArchive; } return new AssetArchive(assets.ToImmutable()); } } throw new InvalidOperationException("Value was marked as Archive, but did not conform to required shape."); } private static Asset DeserializeAsset(Value value) { if (TryGetStringValue(value.StructValue.Fields, Constants.AssetOrArchivePathName, out var path)) return new FileAsset(path); if (TryGetStringValue(value.StructValue.Fields, Constants.AssetOrArchiveUriName, out var uri)) return new RemoteAsset(uri); if (TryGetStringValue(value.StructValue.Fields, Constants.AssetTextName, out var text)) return new StringAsset(text); throw new InvalidOperationException("Value was marked as Asset, but did not conform to required shape."); } private static bool TryDeserializeResource( Value value, [NotNullWhen(true)] out Resource? resource) { if (!IsSpecialStruct(value, out var sig) || sig != Constants.SpecialResourceSig) { resource = null; return false; } if (!TryGetStringValue(value.StructValue.Fields, Constants.ResourceUrnName, out var urn)) { throw new InvalidOperationException("Value was marked as a Resource, but did not conform to required shape."); } if (!TryGetStringValue(value.StructValue.Fields, Constants.ResourceVersionName, out var version)) { version = ""; } var urnParts = urn.Split("::"); var qualifiedType = urnParts[2]; var qualifiedTypeParts = qualifiedType.Split('$'); var type = qualifiedTypeParts[^1]; if (ResourcePackages.TryConstruct(type, version, urn, out resource)) { return true; } resource = new DependencyResource(urn); return true; } private static bool TryGetStringValue( MapField fields, string keyName, [NotNullWhen(true)] out string? result) { if (fields.TryGetValue(keyName, out var value) && value.KindCase == Value.KindOneofCase.StringValue) { result = value.StringValue; return true; } result = null; return false; } } }