|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text.Json;
using Microsoft.NET.Sdk.Localization;
using FXVersion = Microsoft.DotNet.MSBuildSdkResolver.FXVersion;
namespace Microsoft.NET.Sdk.WorkloadManifestReader
{
public partial class WorkloadManifestReader
{
public static WorkloadManifest ReadWorkloadManifest(string manifestId, System.IO.Stream manifestStream, string manifestPath)
=> ReadWorkloadManifest(manifestId, manifestStream, null, manifestPath);
private static void ConsumeToken(ref Utf8JsonStreamReader reader, JsonTokenType expected)
{
if (reader.Read() && expected == reader.TokenType)
{
return;
}
ThrowUnexpectedTokenException(ref reader, expected);
}
private static void ThrowUnexpectedTokenException(ref Utf8JsonStreamReader reader, JsonTokenType expected)
{
string key;
if (expected.IsBool())
{
key = Strings.ExpectedBoolAtOffset;
}
else if (expected.IsInt())
{
key = Strings.ExpectedIntegerAtOffset;
}
else if (expected == JsonTokenType.String)
{
key = Strings.ExpectedStringAtOffset;
}
else
{
throw new WorkloadManifestFormatException(Strings.ExpectedTokenAtOffset, expected, reader.TokenStartIndex);
}
throw new WorkloadManifestFormatException(key, reader.TokenStartIndex);
}
private static string? ReadString(ref Utf8JsonStreamReader reader)
{
ConsumeToken(ref reader, JsonTokenType.String);
return reader.GetString();
}
private static long ReadInt64(ref Utf8JsonStreamReader reader)
{
if (reader.Read() && reader.TokenType.IsInt())
{
if (reader.TryGetInt64(out long value))
{
return value;
}
}
throw new WorkloadManifestFormatException(Strings.ExpectedIntegerAtOffset, reader.TokenStartIndex);
}
private static bool ReadBool(ref Utf8JsonStreamReader reader)
{
if (reader.Read() && reader.TokenType.IsBool())
{
return reader.GetBool();
}
throw new WorkloadManifestFormatException(Strings.ExpectedBoolAtOffset, reader.TokenStartIndex);
}
private static void ThrowDuplicateKeyException<T>(ref Utf8JsonStreamReader reader, T key)
=> throw new WorkloadManifestFormatException(Strings.DuplicateKeyAtOffset, key?.ToString() ?? throw new ArgumentNullException(nameof(key)), reader.TokenStartIndex);
private static WorkloadManifest ReadWorkloadManifest(
string id, string manifestPath,
LocalizationCatalog? localizationCatalog,
ref Utf8JsonStreamReader reader)
{
ConsumeToken(ref reader, JsonTokenType.StartObject);
FXVersion? version = null;
string? description = null;
Dictionary<WorkloadId, BaseWorkloadDefinition>? workloads = null;
Dictionary<WorkloadPackId, WorkloadPack>? packs = null;
Dictionary<string, FXVersion>? dependsOn = null;
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.PropertyName:
var propName = reader.GetString();
if (string.Equals("version", propName, StringComparison.OrdinalIgnoreCase))
{
if (version != null) ThrowDuplicateKeyException(ref reader, propName);
if (reader.Read())
{
if (reader.TokenType == JsonTokenType.String)
{
if (FXVersion.TryParse(reader.GetString(), out version))
{
continue;
}
}
else if (reader.TokenType.IsInt())
{
// older manifests could have an int value
if (reader.TryGetInt64(out var intVersion) && intVersion < int.MaxValue)
{
version = new FXVersion((int)intVersion, 0, 0);
continue;
}
}
}
throw new WorkloadManifestFormatException(Strings.MissingOrInvalidManifestVersion);
}
if (string.Equals("description", propName, StringComparison.OrdinalIgnoreCase))
{
if (description != null) ThrowDuplicateKeyException(ref reader, propName);
description = ReadString(ref reader);
continue;
}
if (string.Equals("depends-on", propName, StringComparison.OrdinalIgnoreCase))
{
if (dependsOn != null) ThrowDuplicateKeyException(ref reader, propName);
dependsOn = ReadDependsOn(ref reader);
continue;
}
if (string.Equals("workloads", propName, StringComparison.OrdinalIgnoreCase))
{
if (workloads != null) ThrowDuplicateKeyException(ref reader, propName);
workloads = ReadWorkloadDefinitions(ref reader, localizationCatalog);
continue;
}
if (string.Equals("packs", propName, StringComparison.OrdinalIgnoreCase))
{
if (packs != null) ThrowDuplicateKeyException(ref reader, propName);
packs = ReadWorkloadPacks(ref reader);
continue;
}
// just ignore this for now. it's part of the spec but we don't have an API for exposing it as structured data.
if (string.Equals("data", propName, StringComparison.OrdinalIgnoreCase))
{
if (!ConsumeValue(ref reader))
{
break;
}
continue;
}
// it would be more robust to ignore unknown keys but right now we're pretty unforgiving
throw new WorkloadManifestFormatException(Strings.UnknownKeyAtOffset, propName, reader.TokenStartIndex);
case JsonTokenType.EndObject:
if (version == null)
{
throw new WorkloadManifestFormatException(Strings.MissingOrInvalidManifestVersion);
}
return new WorkloadManifest(
id,
version,
description,
manifestPath,
workloads ?? new Dictionary<WorkloadId, BaseWorkloadDefinition>(),
packs ?? new Dictionary<WorkloadPackId, WorkloadPack>(),
dependsOn
);
default:
throw new WorkloadManifestFormatException(Strings.UnexpectedTokenAtOffset, reader.TokenType, reader.TokenStartIndex);
}
}
throw new WorkloadManifestFormatException(Strings.IncompleteDocument);
}
/// <summary>
/// this expects the reader to be before the value token, and leaves it on the last token of the value
/// </summary>
private static bool ConsumeValue(ref Utf8JsonStreamReader reader)
{
if (!reader.Read())
{
return false;
}
var tokenType = reader.TokenType;
if (tokenType != JsonTokenType.StartArray && tokenType != JsonTokenType.StartObject)
{
return true;
}
var depth = reader.CurrentDepth;
do
{
if (!reader.Read())
{
return false;
}
} while (reader.CurrentDepth > depth);
return true;
}
private static Dictionary<string, FXVersion> ReadDependsOn(ref Utf8JsonStreamReader reader)
{
ConsumeToken(ref reader, JsonTokenType.StartObject);
var dependsOn = new Dictionary<string, FXVersion>(StringComparer.OrdinalIgnoreCase);
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.PropertyName:
var dependencyId = reader.GetString() ?? string.Empty;
if (FXVersion.TryParse(ReadString(ref reader), out var dependencyVersion))
{
if (dependsOn.ContainsKey(dependencyId))
{
ThrowDuplicateKeyException(ref reader, dependencyId);
}
dependsOn.Add(dependencyId, dependencyVersion!);
continue;
}
goto default;
case JsonTokenType.EndObject:
return dependsOn;
default:
throw new WorkloadManifestFormatException(Strings.UnexpectedTokenAtOffset, reader.TokenType, reader.TokenStartIndex);
}
}
throw new WorkloadManifestFormatException(Strings.IncompleteDocument);
}
private static Dictionary<WorkloadId, BaseWorkloadDefinition> ReadWorkloadDefinitions(ref Utf8JsonStreamReader reader, LocalizationCatalog? localizationCatalog)
{
ConsumeToken(ref reader, JsonTokenType.StartObject);
var workloads = new Dictionary<WorkloadId, BaseWorkloadDefinition>();
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.PropertyName:
var workloadId = new WorkloadId(reader.GetString() ?? string.Empty);
var workload = ReadWorkloadDefinition(workloadId, ref reader, localizationCatalog);
if (workloads.ContainsKey(workloadId)) ThrowDuplicateKeyException(ref reader, workloadId);
workloads.Add(workloadId, workload);
continue;
case JsonTokenType.EndObject:
return workloads;
default:
throw new WorkloadManifestFormatException(Strings.UnexpectedTokenAtOffset, reader.TokenType, reader.TokenStartIndex);
}
}
throw new WorkloadManifestFormatException(Strings.IncompleteDocument);
}
private static Dictionary<WorkloadPackId, WorkloadPack> ReadWorkloadPacks(ref Utf8JsonStreamReader reader)
{
ConsumeToken(ref reader, JsonTokenType.StartObject);
var packs = new Dictionary<WorkloadPackId, WorkloadPack>();
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.PropertyName:
var packId = new WorkloadPackId(reader.GetString() ?? string.Empty);
var pack = ReadWorkloadPack(packId, ref reader);
if (packs.ContainsKey(packId)) ThrowDuplicateKeyException(ref reader, packId);
packs[packId] = pack;
continue;
case JsonTokenType.EndObject:
return packs;
default:
throw new WorkloadManifestFormatException(Strings.UnexpectedTokenAtOffset, reader.TokenType, reader.TokenStartIndex);
}
}
throw new WorkloadManifestFormatException(Strings.IncompleteDocument);
}
private static List<string> ReadStringArray(ref Utf8JsonStreamReader reader)
{
ConsumeToken(ref reader, JsonTokenType.StartArray);
var list = new List<string>();
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.String:
list.Add(reader.GetString() ?? string.Empty);
continue;
case JsonTokenType.EndArray:
return list;
default:
throw new WorkloadManifestFormatException(Strings.UnexpectedTokenAtOffset, reader.TokenType, reader.TokenStartIndex);
}
}
throw new WorkloadManifestFormatException(Strings.IncompleteDocument);
}
private static List<T> ReadStringArray<T>(ref Utf8JsonStreamReader reader, Func<string, T> map)
{
ConsumeToken(ref reader, JsonTokenType.StartArray);
var list = new List<T>();
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.String:
list.Add(map(reader.GetString() ?? string.Empty));
continue;
case JsonTokenType.EndArray:
return list;
default:
throw new WorkloadManifestFormatException(Strings.UnexpectedTokenAtOffset, reader.TokenType, reader.TokenStartIndex);
}
}
throw new WorkloadManifestFormatException(Strings.IncompleteDocument);
}
private static Dictionary<string, string> ReadStringDictionary(ref Utf8JsonStreamReader reader)
{
ConsumeToken(ref reader, JsonTokenType.StartObject);
var dictionary = new Dictionary<string, string>();
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.PropertyName:
var name = reader.GetString() ?? string.Empty;
var val = ReadString(ref reader);
if (dictionary.ContainsKey(name)) ThrowDuplicateKeyException(ref reader, name);
dictionary.Add(name, val ?? string.Empty);
continue;
case JsonTokenType.EndObject:
return dictionary;
default:
throw new WorkloadManifestFormatException(Strings.UnexpectedTokenAtOffset, reader.TokenType, reader.TokenStartIndex);
}
}
throw new WorkloadManifestFormatException(Strings.IncompleteDocument);
}
private static Dictionary<string, TValue> ReadStringDictionary<TValue>(ref Utf8JsonStreamReader reader, Func<string, TValue> mapValue)
{
ConsumeToken(ref reader, JsonTokenType.StartObject);
var dictionary = new Dictionary<string, TValue>();
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.PropertyName:
var name = reader.GetString() ?? string.Empty;
var val = mapValue(ReadString(ref reader) ?? string.Empty);
if (dictionary.ContainsKey(name)) ThrowDuplicateKeyException(ref reader, name);
dictionary.Add(name, val);
continue;
case JsonTokenType.EndObject:
return dictionary;
default:
throw new WorkloadManifestFormatException(Strings.UnexpectedTokenAtOffset, reader.TokenType, reader.TokenStartIndex);
}
}
throw new WorkloadManifestFormatException(Strings.IncompleteDocument);
}
private static BaseWorkloadDefinition ReadWorkloadDefinition(WorkloadId id, ref Utf8JsonStreamReader reader, LocalizationCatalog? localizationCatalog)
{
ConsumeToken(ref reader, JsonTokenType.StartObject);
string? description = null;
bool? isAbstractOrNull = null;
WorkloadDefinitionKind? kind = null;
List<WorkloadPackId>? packs = null;
List<WorkloadId>? extends = null;
List<string>? platforms = null;
WorkloadId? replaceWith = null;
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.PropertyName:
var propName = reader.GetString();
if (string.Equals("description", propName, StringComparison.OrdinalIgnoreCase))
{
if (description != null) ThrowDuplicateKeyException(ref reader, propName);
description = ReadStringLocalized(ref reader, localizationCatalog, $"workloads/{id}/description");
continue;
}
if (string.Equals("kind", propName, StringComparison.OrdinalIgnoreCase))
{
if (kind != null) ThrowDuplicateKeyException(ref reader, propName);
var kindStr = ReadString(ref reader);
if (Enum.TryParse<WorkloadDefinitionKind>(kindStr, true, out var parsedKind))
{
kind = parsedKind;
}
else
{
throw new WorkloadManifestFormatException(Strings.UnknownWorkloadDefinitionKind, kindStr, reader.TokenStartIndex);
}
continue;
}
if (string.Equals("abstract", propName, StringComparison.OrdinalIgnoreCase))
{
if (isAbstractOrNull != null) ThrowDuplicateKeyException(ref reader, propName);
isAbstractOrNull = ReadBool(ref reader);
continue;
}
if (string.Equals("packs", propName, StringComparison.OrdinalIgnoreCase))
{
if (packs != null) ThrowDuplicateKeyException(ref reader, propName);
packs = ReadStringArray<WorkloadPackId>(ref reader, s => new WorkloadPackId(s));
continue;
}
if (string.Equals("extends", propName, StringComparison.OrdinalIgnoreCase))
{
if (extends != null) ThrowDuplicateKeyException(ref reader, propName);
extends = ReadStringArray<WorkloadId>(ref reader, s => new WorkloadId(s));
continue;
}
if (string.Equals("platforms", propName, StringComparison.OrdinalIgnoreCase))
{
if (platforms != null) ThrowDuplicateKeyException(ref reader, propName);
platforms = ReadStringArray(ref reader);
continue;
}
if (string.Equals("replace-with", propName, StringComparison.OrdinalIgnoreCase))
{
if (replaceWith != null) ThrowDuplicateKeyException(ref reader, propName);
replaceWith = new WorkloadId(ReadString(ref reader) ?? string.Empty);
continue;
}
throw new WorkloadManifestFormatException(Strings.UnknownKeyAtOffset, propName, reader.TokenStartIndex);
case JsonTokenType.EndObject:
if (replaceWith is WorkloadId replacementId)
{
if (isAbstractOrNull != null || description != null || kind != null || extends != null || packs != null || platforms != null)
{
throw new WorkloadManifestFormatException(Strings.RedirectWorkloadHasOtherKeys, id);
}
throw new NotImplementedException("Workload redirects are not yet fully implemented");
//return new WorkloadRedirect (id, replacementId);
}
var isAbstract = isAbstractOrNull ?? false;
if (!isAbstract && kind == WorkloadDefinitionKind.Dev && string.IsNullOrEmpty(description))
{
throw new WorkloadManifestFormatException(Strings.ConcreteWorkloadHasNoDescription, id);
}
return new WorkloadDefinition(id, isAbstract, description, kind ?? WorkloadDefinitionKind.Dev, extends, packs, platforms);
default:
throw new WorkloadManifestFormatException(Strings.UnexpectedTokenAtOffset, reader.TokenType, reader.TokenStartIndex);
}
}
throw new WorkloadManifestFormatException(Strings.IncompleteDocument);
}
private static WorkloadPack ReadWorkloadPack(WorkloadPackId id, ref Utf8JsonStreamReader reader)
{
ConsumeToken(ref reader, JsonTokenType.StartObject);
string? version = null;
WorkloadPackKind? kind = null;
Dictionary<string, WorkloadPackId>? aliasTo = null;
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.PropertyName:
var propName = reader.GetString();
if (string.Equals("version", propName, StringComparison.OrdinalIgnoreCase))
{
if (version != null) ThrowDuplicateKeyException(ref reader, propName);
version = ReadString(ref reader);
continue;
}
if (string.Equals("kind", propName, StringComparison.OrdinalIgnoreCase))
{
if (kind != null) ThrowDuplicateKeyException(ref reader, propName);
var kindStr = ReadString(ref reader);
if (Enum.TryParse<WorkloadPackKind>(kindStr, true, out var parsedKind))
{
kind = parsedKind;
}
else
{
throw new WorkloadManifestFormatException(Strings.UnknownWorkloadPackKind, kindStr, reader.TokenStartIndex);
}
continue;
}
if (string.Equals("alias-to", propName, StringComparison.OrdinalIgnoreCase))
{
if (aliasTo != null) ThrowDuplicateKeyException(ref reader, propName);
aliasTo = ReadStringDictionary(ref reader, s => new WorkloadPackId(s));
continue;
}
throw new WorkloadManifestFormatException(Strings.UnknownKeyAtOffset, propName, reader.TokenStartIndex);
case JsonTokenType.EndObject:
if (version == null)
{
throw new WorkloadManifestFormatException(Strings.MissingWorkloadPackVersion, id);
}
if (kind == null)
{
throw new WorkloadManifestFormatException(Strings.MissingWorkloadPackKind, id);
}
return new WorkloadPack(id, version, kind.Value, aliasTo);
default:
throw new WorkloadManifestFormatException(Strings.UnexpectedTokenAtOffset, reader.TokenType, reader.TokenStartIndex);
}
}
throw new WorkloadManifestFormatException(Strings.IncompleteDocument);
}
}
}
|