Compare updated template evaluator (#4092)

This commit is contained in:
eric sciple
2025-11-07 14:18:52 -06:00
committed by GitHub
parent 53d69ff441
commit b5b7986cd6
188 changed files with 27222 additions and 4 deletions
@@ -0,0 +1,21 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using GitHub.Actions.Expressions.Sdk;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating
{
/// <summary>
/// This expression node retrieves a user-defined named-value. This is used during expression evaluation.
/// </summary>
internal sealed class ContextValueNode : NamedValue
{
protected override Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory)
{
resultMemory = null;
return (context.State as TemplateContext).ExpressionValues[Name];
}
}
}
@@ -0,0 +1,25 @@
using System;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating
{
internal class EmptyTraceWriter : ITraceWriter
{
public void Error(
String format,
params Object[] args)
{
}
public void Info(
String format,
params Object[] args)
{
}
public void Verbose(
String format,
params Object[] args)
{
}
}
}
@@ -0,0 +1,27 @@
using System;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating
{
/// <summary>
/// Wraps an ITraceWriter so it can be passed for expression evaluation.
/// </summary>
internal sealed class ExpressionTraceWriter : GitHub.Actions.Expressions.ITraceWriter
{
public ExpressionTraceWriter(ITraceWriter trace)
{
m_trace = trace;
}
public void Info(String message)
{
m_trace.Info("{0}", message);
}
public void Verbose(String message)
{
m_trace.Verbose("{0}", message);
}
private readonly ITraceWriter m_trace;
}
}
@@ -0,0 +1,26 @@
using System;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating
{
/// <summary>
/// Interface for reading a source object (or file).
/// This interface is used by TemplateReader to build a TemplateToken DOM.
/// </summary>
internal interface IObjectReader
{
Boolean AllowLiteral(out LiteralToken token);
Boolean AllowSequenceStart(out SequenceToken token);
Boolean AllowSequenceEnd();
Boolean AllowMappingStart(out MappingToken token);
Boolean AllowMappingEnd();
void ValidateStart();
void ValidateEnd();
}
}
@@ -0,0 +1,31 @@
using System;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating
{
/// <summary>
/// Interface for building an object. This interface is used by
/// TemplateWriter to convert a TemplateToken DOM to another format.
/// </summary>
internal interface IObjectWriter
{
void WriteNull();
void WriteBoolean(Boolean value);
void WriteNumber(Double value);
void WriteString(String value);
void WriteSequenceStart();
void WriteSequenceEnd();
void WriteMappingStart();
void WriteMappingEnd();
void WriteStart();
void WriteEnd();
}
}
@@ -0,0 +1,19 @@
using System;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating
{
public interface ITraceWriter
{
void Error(
String format,
params Object[] args);
void Info(
String format,
params Object[] args);
void Verbose(
String format,
params Object[] args);
}
}
@@ -0,0 +1,10 @@
namespace GitHub.Actions.WorkflowParser.ObjectTemplating
{
internal static class ITraceWriterExtensions
{
internal static GitHub.Actions.Expressions.ITraceWriter ToExpressionTraceWriter(this ITraceWriter trace)
{
return new ExpressionTraceWriter(trace);
}
}
}
@@ -0,0 +1,57 @@
using System;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Schema
{
internal sealed class BooleanDefinition : ScalarDefinition
{
internal BooleanDefinition()
{
}
internal BooleanDefinition(MappingToken definition)
: base(definition)
{
foreach (var definitionPair in definition)
{
var definitionKey = definitionPair.Key.AssertString($"{TemplateConstants.Definition} key");
switch (definitionKey.Value)
{
case TemplateConstants.Boolean:
var mapping = definitionPair.Value.AssertMapping($"{TemplateConstants.Definition} {TemplateConstants.Boolean}");
foreach (var mappingPair in mapping)
{
var mappingKey = mappingPair.Key.AssertString($"{TemplateConstants.Definition} {TemplateConstants.Boolean} key");
switch (mappingKey.Value)
{
default:
mappingKey.AssertUnexpectedValue($"{TemplateConstants.Definition} {TemplateConstants.Boolean} key");
break;
}
}
break;
case TemplateConstants.CoerceRaw:
continue;
default:
definitionKey.AssertUnexpectedValue($"{TemplateConstants.Definition} key");
break;
}
}
}
internal override DefinitionType DefinitionType => DefinitionType.Boolean;
internal override Boolean IsMatch(LiteralToken literal)
{
return literal is BooleanToken;
}
internal override void Validate(
TemplateSchema schema,
String name)
{
}
}
}
@@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Linq;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Schema
{
/// <summary>
/// Defines the allowable schema for a user defined type
/// </summary>
internal abstract class Definition
{
protected Definition()
{
}
protected Definition(MappingToken definition)
{
for (var i = 0; i < definition.Count; )
{
var definitionKey = definition[i].Key.AssertString($"{TemplateConstants.Definition} key");
if (String.Equals(definitionKey.Value, TemplateConstants.Context, StringComparison.Ordinal))
{
var context = definition[i].Value.AssertSequence($"{TemplateConstants.Context}");
definition.RemoveAt(i);
var readerContext = new HashSet<String>(StringComparer.OrdinalIgnoreCase);
var evaluatorContext = new HashSet<String>(StringComparer.OrdinalIgnoreCase);
foreach (TemplateToken item in context)
{
var itemStr = item.AssertString($"{TemplateConstants.Context} item").Value;
readerContext.Add(itemStr);
// Remove min/max parameter info
var paramIndex = itemStr.IndexOf('(');
if (paramIndex > 0)
{
evaluatorContext.Add(String.Concat(itemStr.Substring(0, paramIndex + 1), ")"));
}
else
{
evaluatorContext.Add(itemStr);
}
}
ReaderContext = readerContext.ToArray();
EvaluatorContext = evaluatorContext.ToArray();
}
else if (String.Equals(definitionKey.Value, TemplateConstants.Description, StringComparison.Ordinal))
{
definition.RemoveAt(i);
}
else
{
i++;
}
}
}
internal abstract DefinitionType DefinitionType { get; }
/// <summary>
/// Used by the template reader to determine allowed expression values and functions.
/// Also used by the template reader to validate function min/max parameters.
/// </summary>
internal String[] ReaderContext { get; private set; } = new String[0];
/// <summary>
/// Used by the template evaluator to determine allowed expression values and functions.
/// The min/max parameter info is omitted.
/// </summary>
internal String[] EvaluatorContext { get; private set; } = new String[0];
internal abstract void Validate(
TemplateSchema schema,
String name);
}
}
@@ -0,0 +1,16 @@
using System;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Schema
{
internal enum DefinitionType
{
Null,
Boolean,
Number,
String,
Sequence,
Mapping,
OneOf,
AllowedValues,
}
}
@@ -0,0 +1,111 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using System.Collections.Generic;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Schema
{
internal sealed class MappingDefinition : Definition
{
internal MappingDefinition()
{
}
internal MappingDefinition(MappingToken definition)
: base(definition)
{
foreach (var definitionPair in definition)
{
var definitionKey = definitionPair.Key.AssertString($"{TemplateConstants.Definition} key");
switch (definitionKey.Value)
{
case TemplateConstants.Mapping:
var mapping = definitionPair.Value.AssertMapping($"{TemplateConstants.Definition} {TemplateConstants.Mapping}");
foreach (var mappingPair in mapping)
{
var mappingKey = mappingPair.Key.AssertString($"{TemplateConstants.Definition} {TemplateConstants.Mapping} key");
switch (mappingKey.Value)
{
case TemplateConstants.Properties:
var properties = mappingPair.Value.AssertMapping($"{TemplateConstants.Definition} {TemplateConstants.Mapping} {TemplateConstants.Properties}");
foreach (var propertiesPair in properties)
{
var propertyName = propertiesPair.Key.AssertString($"{TemplateConstants.Definition} {TemplateConstants.Mapping} {TemplateConstants.Properties} key");
Properties.Add(propertyName.Value, new PropertyDefinition(propertiesPair.Value));
}
break;
case TemplateConstants.LooseKeyType:
var looseKeyType = mappingPair.Value.AssertString($"{TemplateConstants.Definition} {TemplateConstants.Mapping} {TemplateConstants.LooseKeyType}");
LooseKeyType = looseKeyType.Value;
break;
case TemplateConstants.LooseValueType:
var looseValueType = mappingPair.Value.AssertString($"{TemplateConstants.Definition} {TemplateConstants.Mapping} {TemplateConstants.LooseValueType}");
LooseValueType = looseValueType.Value;
break;
default:
definitionKey.AssertUnexpectedValue($"{TemplateConstants.Definition} key");
break;
}
}
break;
case TemplateConstants.CoerceRaw:
continue;
default:
definitionKey.AssertUnexpectedValue($"{TemplateConstants.Definition} key");
break;
}
}
}
internal override DefinitionType DefinitionType => DefinitionType.Mapping;
internal String LooseKeyType { get; set; }
internal String LooseValueType { get; set; }
internal Dictionary<String, PropertyDefinition> Properties { get; } = new Dictionary<String, PropertyDefinition>(StringComparer.Ordinal);
internal override void Validate(
TemplateSchema schema,
String name)
{
// Lookup loose key type
if (!String.IsNullOrEmpty(LooseKeyType))
{
schema.GetDefinition(LooseKeyType);
// Lookup loose value type
if (!String.IsNullOrEmpty(LooseValueType))
{
schema.GetDefinition(LooseValueType);
}
else
{
throw new ArgumentException($"Property '{TemplateConstants.LooseKeyType}' is defined but '{TemplateConstants.LooseValueType}' is not defined on '{name}'");
}
}
// Otherwise validate loose value type not be defined
else if (!String.IsNullOrEmpty(LooseValueType))
{
throw new ArgumentException($"Property '{TemplateConstants.LooseValueType}' is defined but '{TemplateConstants.LooseKeyType}' is not defined");
}
// Lookup each property
foreach (var property in Properties)
{
if (String.IsNullOrEmpty(property.Value.Type))
{
throw new ArgumentException($"Type not specified for the '{property.Key}' property on the '{name}' type");
}
schema.GetDefinition(property.Value.Type);
}
}
}
}
@@ -0,0 +1,58 @@
using System;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Schema
{
internal sealed class NullDefinition : ScalarDefinition
{
internal NullDefinition()
{
}
internal NullDefinition(MappingToken definition)
: base(definition)
{
foreach (var definitionPair in definition)
{
var definitionKey = definitionPair.Key.AssertString($"{TemplateConstants.Definition} key");
switch (definitionKey.Value)
{
case TemplateConstants.Null:
var mapping = definitionPair.Value.AssertMapping($"{TemplateConstants.Definition} {TemplateConstants.Null}");
foreach (var mappingPair in mapping)
{
var mappingKey = mappingPair.Key.AssertString($"{TemplateConstants.Definition} {TemplateConstants.Null} key");
switch (mappingKey.Value)
{
default:
mappingKey.AssertUnexpectedValue($"{TemplateConstants.Definition} {TemplateConstants.Null} key");
break;
}
}
break;
case TemplateConstants.AllowedValues:
case TemplateConstants.CoerceRaw:
continue;
default:
definitionKey.AssertUnexpectedValue($"{TemplateConstants.Definition} key");
break;
}
}
}
internal override DefinitionType DefinitionType => DefinitionType.Null;
internal override Boolean IsMatch(LiteralToken literal)
{
return literal is NullToken;
}
internal override void Validate(
TemplateSchema schema,
String name)
{
}
}
}
@@ -0,0 +1,57 @@
using System;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Schema
{
internal sealed class NumberDefinition : ScalarDefinition
{
internal NumberDefinition()
{
}
internal NumberDefinition(MappingToken definition)
: base(definition)
{
foreach (var definitionPair in definition)
{
var definitionKey = definitionPair.Key.AssertString($"{TemplateConstants.Definition} key");
switch (definitionKey.Value)
{
case TemplateConstants.Number:
var mapping = definitionPair.Value.AssertMapping($"{TemplateConstants.Definition} {TemplateConstants.Number}");
foreach (var mappingPair in mapping)
{
var mappingKey = mappingPair.Key.AssertString($"{TemplateConstants.Definition} {TemplateConstants.Number} key");
switch (mappingKey.Value)
{
default:
mappingKey.AssertUnexpectedValue($"{TemplateConstants.Definition} {TemplateConstants.Number} key");
break;
}
}
break;
case TemplateConstants.CoerceRaw:
continue;
default:
definitionKey.AssertUnexpectedValue($"{TemplateConstants.Definition} key");
break;
}
}
}
internal override DefinitionType DefinitionType => DefinitionType.Number;
internal override Boolean IsMatch(LiteralToken literal)
{
return literal is NumberToken;
}
internal override void Validate(
TemplateSchema schema,
String name)
{
}
}
}
@@ -0,0 +1,191 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using System.Collections.Generic;
using System.Linq;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Schema
{
/// <summary>
/// Must resolve to exactly one of the referenced definitions
/// </summary>
internal sealed class OneOfDefinition : Definition
{
internal OneOfDefinition()
{
}
internal OneOfDefinition(MappingToken definition)
: base(definition)
{
foreach (var definitionPair in definition)
{
var definitionKey = definitionPair.Key.AssertString($"{TemplateConstants.Definition} key");
switch (definitionKey.Value)
{
case TemplateConstants.OneOf:
var oneOf = definitionPair.Value.AssertSequence(TemplateConstants.OneOf);
foreach (var oneOfItem in oneOf)
{
var reference = oneOfItem.AssertString(TemplateConstants.OneOf);
OneOf.Add(reference.Value);
}
break;
case TemplateConstants.CoerceRaw:
continue;
default:
definitionKey.AssertUnexpectedValue($"{TemplateConstants.Definition} key");
break;
}
}
}
internal override DefinitionType DefinitionType => DefinitionType.OneOf;
internal List<String> OneOf { get; } = new List<String>();
internal override void Validate(
TemplateSchema schema,
String name)
{
if (OneOf.Count == 0)
{
throw new ArgumentException($"'{name}' does not contain any references");
}
var foundLooseKeyType = false;
var mappingDefinitions = default(List<MappingDefinition>);
var sequenceDefinition = default(SequenceDefinition);
var nullDefinition = default(NullDefinition);
var booleanDefinition = default(BooleanDefinition);
var numberDefinition = default(NumberDefinition);
var stringDefinitions = default(List<StringDefinition>);
foreach (var nestedType in OneOf)
{
var nestedDefinition = schema.GetDefinition(nestedType);
if (nestedDefinition.ReaderContext.Length > 0)
{
throw new ArgumentException($"'{name}' is a one-of definition and references another definition that defines context. This is currently not supported.");
}
if (nestedDefinition is MappingDefinition mappingDefinition)
{
if (mappingDefinitions == null)
{
mappingDefinitions = new List<MappingDefinition>();
}
mappingDefinitions.Add(mappingDefinition);
if (!String.IsNullOrEmpty(mappingDefinition.LooseKeyType))
{
foundLooseKeyType = true;
}
}
else if (nestedDefinition is SequenceDefinition s)
{
// Multiple sequence definitions not allowed
if (sequenceDefinition != null)
{
throw new ArgumentException($"'{name}' refers to more than one '{TemplateConstants.Sequence}'");
}
sequenceDefinition = s;
}
else if (nestedDefinition is NullDefinition n)
{
// Multiple sequence definitions not allowed
if (nullDefinition != null)
{
throw new ArgumentException($"'{name}' refers to more than one '{TemplateConstants.Null}'");
}
nullDefinition = n;
}
else if (nestedDefinition is BooleanDefinition b)
{
// Multiple boolean definitions not allowed
if (booleanDefinition != null)
{
throw new ArgumentException($"'{name}' refers to more than one '{TemplateConstants.Boolean}'");
}
booleanDefinition = b;
}
else if (nestedDefinition is NumberDefinition num)
{
// Multiple number definitions not allowed
if (numberDefinition != null)
{
throw new ArgumentException($"'{name}' refers to more than one '{TemplateConstants.Number}'");
}
numberDefinition = num;
}
else if (nestedDefinition is StringDefinition stringDefinition)
{
// First string definition
if (stringDefinitions == null)
{
stringDefinitions = new List<StringDefinition>();
}
// Multiple string definitions, all must be 'Constant'
else if ((stringDefinitions.Count == 1 && String.IsNullOrEmpty(stringDefinitions[0].Constant))
|| String.IsNullOrEmpty(stringDefinition.Constant))
{
throw new ArgumentException($"'{name}' refers to more than one '{TemplateConstants.Scalar}', but some do not set '{TemplateConstants.Constant}'");
}
stringDefinitions.Add(stringDefinition);
}
else if (nestedDefinition is OneOfDefinition oneOfDefinition)
{
// Allow one-of to reference another one-of
}
else
{
throw new ArgumentException($"'{name}' refers to a '{nestedDefinition.DefinitionType}' definition");
}
}
if (mappingDefinitions?.Count > 1)
{
if (foundLooseKeyType)
{
throw new ArgumentException($"'{name}' refers to two mappings that both set '{TemplateConstants.LooseKeyType}'");
}
var seenProperties = new Dictionary<String, PropertyDefinition>(StringComparer.Ordinal);
foreach (var mappingDefinition in mappingDefinitions)
{
foreach (var newProperty in mappingDefinition.Properties)
{
// Already seen
if (seenProperties.TryGetValue(newProperty.Key, out PropertyDefinition existingProperty))
{
// Types match
if (String.Equals(existingProperty.Type, newProperty.Value.Type, StringComparison.Ordinal))
{
continue;
}
// Collision
throw new ArgumentException($"'{name}' contains two mappings with the same property, but each refers to a different type. All matching properties must refer to the same type.");
}
// New
else
{
seenProperties.Add(newProperty.Key, newProperty.Value);
}
}
}
}
}
}
}
@@ -0,0 +1,47 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Schema
{
internal sealed class PropertyDefinition
{
internal PropertyDefinition(TemplateToken token)
{
if (token is StringToken stringToken)
{
Type = stringToken.Value;
}
else
{
var mapping = token.AssertMapping($"{TemplateConstants.MappingPropertyValue}");
foreach (var mappingPair in mapping)
{
var mappingKey = mappingPair.Key.AssertString($"{TemplateConstants.MappingPropertyValue} key");
switch (mappingKey.Value)
{
case TemplateConstants.Type:
Type = mappingPair.Value.AssertString($"{TemplateConstants.MappingPropertyValue} {TemplateConstants.Type}").Value;
break;
case TemplateConstants.Required:
Required = mappingPair.Value.AssertBoolean($"{TemplateConstants.MappingPropertyValue} {TemplateConstants.Required}").Value;
break;
case TemplateConstants.Description:
Description = mappingPair.Value.AssertString($"{TemplateConstants.MappingPropertyValue} {TemplateConstants.Description}").Value;
break;
default:
mappingKey.AssertUnexpectedValue($"{TemplateConstants.MappingPropertyValue} key");
break;
}
}
}
}
internal String Type { get; set; }
internal Boolean Required { get; set; }
internal String Description { get; set; }
}
}
@@ -0,0 +1,19 @@
using System;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Schema
{
internal abstract class ScalarDefinition : Definition
{
internal ScalarDefinition()
{
}
internal ScalarDefinition(MappingToken definition)
: base(definition)
{
}
internal abstract Boolean IsMatch(LiteralToken literal);
}
}
@@ -0,0 +1,69 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Schema
{
internal sealed class SequenceDefinition : Definition
{
internal SequenceDefinition()
{
}
internal SequenceDefinition(MappingToken definition)
: base(definition)
{
foreach (var definitionPair in definition)
{
var definitionKey = definitionPair.Key.AssertString($"{TemplateConstants.Definition} key");
switch (definitionKey.Value)
{
case TemplateConstants.Sequence:
var mapping = definitionPair.Value.AssertMapping($"{TemplateConstants.Definition} {TemplateConstants.Sequence}");
foreach (var mappingPair in mapping)
{
var mappingKey = mappingPair.Key.AssertString($"{TemplateConstants.Definition} {TemplateConstants.Sequence} key");
switch (mappingKey.Value)
{
case TemplateConstants.ItemType:
var itemType = mappingPair.Value.AssertString($"{TemplateConstants.Definition} {TemplateConstants.Sequence} {TemplateConstants.ItemType}");
ItemType = itemType.Value;
break;
default:
mappingKey.AssertUnexpectedValue($"{TemplateConstants.Definition} {TemplateConstants.Sequence} key");
break;
}
}
break;
case TemplateConstants.CoerceRaw:
continue;
default:
definitionKey.AssertUnexpectedValue($"{TemplateConstants.Definition} key");
break;
}
}
}
internal override DefinitionType DefinitionType => DefinitionType.Sequence;
internal String ItemType { get; set; }
internal override void Validate(
TemplateSchema schema,
String name)
{
if (String.IsNullOrEmpty(ItemType))
{
throw new ArgumentException($"'{name}' does not define '{TemplateConstants.ItemType}'");
}
// Lookup item type
schema.GetDefinition(ItemType);
}
}
}
@@ -0,0 +1,116 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Schema
{
internal sealed class StringDefinition : ScalarDefinition
{
internal StringDefinition()
{
}
internal StringDefinition(MappingToken definition)
: base(definition)
{
foreach (var definitionPair in definition)
{
var definitionKey = definitionPair.Key.AssertString($"{TemplateConstants.Definition} key");
switch (definitionKey.Value)
{
case TemplateConstants.String:
var mapping = definitionPair.Value.AssertMapping($"{TemplateConstants.Definition} {TemplateConstants.String}");
foreach (var mappingPair in mapping)
{
var mappingKey = mappingPair.Key.AssertString($"{TemplateConstants.Definition} {TemplateConstants.String} key");
switch (mappingKey.Value)
{
case TemplateConstants.Constant:
var constantStringToken = mappingPair.Value.AssertString($"{TemplateConstants.Definition} {TemplateConstants.String} {TemplateConstants.Constant}");
Constant = constantStringToken.Value;
break;
case TemplateConstants.IgnoreCase:
var ignoreCaseBooleanToken = mappingPair.Value.AssertBoolean($"{TemplateConstants.Definition} {TemplateConstants.String} {TemplateConstants.IgnoreCase}");
IgnoreCase = ignoreCaseBooleanToken.Value;
break;
case TemplateConstants.RequireNonEmpty:
var requireNonEmptyBooleanToken = mappingPair.Value.AssertBoolean($"{TemplateConstants.Definition} {TemplateConstants.String} {TemplateConstants.RequireNonEmpty}");
RequireNonEmpty = requireNonEmptyBooleanToken.Value;
break;
case TemplateConstants.IsExpression:
var isExpressionBooleanToken = mappingPair.Value.AssertBoolean($"{TemplateConstants.Definition} {TemplateConstants.String} {TemplateConstants.IsExpression}");
IsExpression = isExpressionBooleanToken.Value;
break;
default:
mappingKey.AssertUnexpectedValue($"{TemplateConstants.Definition} {TemplateConstants.String} key");
break;
}
}
break;
case TemplateConstants.CoerceRaw:
continue;
default:
definitionKey.AssertUnexpectedValue($"{TemplateConstants.Definition} key");
break;
}
}
}
internal override DefinitionType DefinitionType => DefinitionType.String;
internal String Constant { get; set; }
internal Boolean IgnoreCase { get; set; }
internal Boolean RequireNonEmpty { get; set; }
internal Boolean IsExpression { get; set; }
internal override Boolean IsMatch(LiteralToken literal)
{
if (literal is StringToken str)
{
var value = str.Value;
if (!String.IsNullOrEmpty(Constant))
{
var comparison = IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
if (String.Equals(Constant, value, comparison))
{
return true;
}
}
else if (RequireNonEmpty)
{
if (!String.IsNullOrEmpty(value))
{
return true;
}
}
else
{
return true;
}
}
return false;
}
internal override void Validate(
TemplateSchema schema,
String name)
{
if (!String.IsNullOrEmpty(Constant) && RequireNonEmpty)
{
throw new ArgumentException($"Properties '{TemplateConstants.Constant}' and '{TemplateConstants.RequireNonEmpty}' cannot both be set");
}
}
}
}
@@ -0,0 +1,484 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Schema
{
/// <summary>
/// This models the root schema object and contains definitions
/// </summary>
public sealed class TemplateSchema
{
internal TemplateSchema()
: this(null)
{
}
private TemplateSchema(MappingToken mapping)
{
// Add built-in type: null
var nullDefinition = new NullDefinition();
Definitions.Add(TemplateConstants.Null, nullDefinition);
// Add built-in type: boolean
var booleanDefinition = new BooleanDefinition();
Definitions.Add(TemplateConstants.Boolean, booleanDefinition);
// Add built-in type: number
var numberDefinition = new NumberDefinition();
Definitions.Add(TemplateConstants.Number, numberDefinition);
// Add built-in type: string
var stringDefinition = new StringDefinition();
Definitions.Add(TemplateConstants.String, stringDefinition);
// Add built-in type: sequence
var sequenceDefinition = new SequenceDefinition { ItemType = TemplateConstants.Any };
Definitions.Add(TemplateConstants.Sequence, sequenceDefinition);
// Add built-in type: mapping
var mappingDefinition = new MappingDefinition { LooseKeyType = TemplateConstants.String, LooseValueType = TemplateConstants.Any };
Definitions.Add(TemplateConstants.Mapping, mappingDefinition);
// Add built-in type: any
var anyDefinition = new OneOfDefinition();
anyDefinition.OneOf.Add(TemplateConstants.Null);
anyDefinition.OneOf.Add(TemplateConstants.Boolean);
anyDefinition.OneOf.Add(TemplateConstants.Number);
anyDefinition.OneOf.Add(TemplateConstants.String);
anyDefinition.OneOf.Add(TemplateConstants.Sequence);
anyDefinition.OneOf.Add(TemplateConstants.Mapping);
Definitions.Add(TemplateConstants.Any, anyDefinition);
if (mapping != null)
{
foreach (var pair in mapping)
{
var key = pair.Key.AssertString($"{TemplateConstants.TemplateSchema} key");
switch (key.Value)
{
case TemplateConstants.Version:
var version = pair.Value.AssertString(TemplateConstants.Version);
Version = version.Value;
break;
case TemplateConstants.Definitions:
var definitions = pair.Value.AssertMapping(TemplateConstants.Definitions);
foreach (var definitionsPair in definitions)
{
var definitionsKey = definitionsPair.Key.AssertString($"{TemplateConstants.Definitions} key");
var definitionsValue = definitionsPair.Value.AssertMapping(TemplateConstants.Definition);
var definition = default(Definition);
foreach (var definitionPair in definitionsValue)
{
var definitionKey = definitionPair.Key.AssertString($"{TemplateConstants.Definition} key");
switch (definitionKey.Value)
{
case TemplateConstants.Null:
definition = new NullDefinition(definitionsValue);
break;
case TemplateConstants.Boolean:
definition = new BooleanDefinition(definitionsValue);
break;
case TemplateConstants.Number:
definition = new NumberDefinition(definitionsValue);
break;
case TemplateConstants.String:
definition = new StringDefinition(definitionsValue);
break;
case TemplateConstants.Sequence:
definition = new SequenceDefinition(definitionsValue);
break;
case TemplateConstants.Mapping:
definition = new MappingDefinition(definitionsValue);
break;
case TemplateConstants.OneOf:
definition = new OneOfDefinition(definitionsValue);
break;
case TemplateConstants.AllowedValues:
// Ignore allowed-values in CSharp parser, we don't need to support events here
definition = new NullDefinition(definitionsValue);
break;
case TemplateConstants.CoerceRaw:
case TemplateConstants.Context:
case TemplateConstants.Description:
continue;
default:
definitionKey.AssertUnexpectedValue("definition mapping key"); // throws
break;
}
break;
}
if (definition == null)
{
throw new ArgumentException($"Unable to determine definition details. Specify the '{TemplateConstants.Structure}' property");
}
Definitions.Add(definitionsKey.Value, definition);
}
break;
default:
key.AssertUnexpectedValue($"{TemplateConstants.TemplateSchema} key"); // throws
break;
}
}
}
}
internal Dictionary<String, Definition> Definitions { get; } = new Dictionary<String, Definition>(StringComparer.Ordinal);
internal String Version { get; }
/// <summary>
/// Loads a user's schema file
/// </summary>
internal static TemplateSchema Load(IObjectReader objectReader)
{
var context = new TemplateContext
{
CancellationToken = CancellationToken.None,
Errors = new TemplateValidationErrors(maxErrors: 10, maxMessageLength: 500),
Memory = new TemplateMemory(
maxDepth: 50,
maxEvents: 1000000, // 1 million
maxBytes: 1024 * 1024), // 1 mb
TraceWriter = new EmptyTraceWriter(),
};
var value = TemplateReader.Read(context, TemplateConstants.TemplateSchema, objectReader, null, Schema, out _);
if (context.Errors.Count > 0)
{
throw new TemplateValidationException(context.Errors);
}
var mapping = value.AssertMapping(TemplateConstants.TemplateSchema);
var schema = new TemplateSchema(mapping);
schema.Validate();
return schema;
}
internal IEnumerable<T> Get<T>(Definition definition)
where T : Definition
{
if (definition is T match)
{
yield return match;
}
else if (definition is OneOfDefinition oneOf)
{
foreach (var reference in oneOf.OneOf)
{
var nestedDefinition = GetDefinition(reference);
if (nestedDefinition is T match2)
{
yield return match2;
}
}
}
}
internal Definition GetDefinition(String type)
{
if (Definitions.TryGetValue(type, out Definition value))
{
return value;
}
throw new ArgumentException($"Schema definition '{type}' not found");
}
internal Boolean HasProperties(MappingDefinition definition)
{
return definition.Properties.Count > 0;
}
internal Boolean TryGetProperty(
MappingDefinition definition,
String name,
out String type)
{
if (definition.Properties.TryGetValue(name, out PropertyDefinition property))
{
type = property.Type;
return true;
}
type = null;
return false;
}
internal Boolean TryMatchKey(
List<MappingDefinition> definitions,
String key,
out String valueType)
{
valueType = null;
// Check for a matching well known property
var notFoundInSome = false;
for (var i = 0; i < definitions.Count; i++)
{
var definition = definitions[i];
if (TryGetProperty(definition, key, out String t))
{
if (valueType == null)
{
valueType = t;
}
}
else
{
notFoundInSome = true;
}
}
// Check if found
if (valueType != null)
{
// Filter the matched definitions if needed
if (notFoundInSome)
{
for (var i = 0; i < definitions.Count;)
{
if (TryGetProperty(definitions[i], key, out _))
{
i++;
}
else
{
definitions.RemoveAt(i);
}
}
}
return true;
}
return false;
}
/// <summary>
/// The built-in schema for reading schema files
/// </summary>
private static TemplateSchema Schema
{
get
{
if (s_schema == null)
{
var schema = new TemplateSchema();
StringDefinition stringDefinition;
SequenceDefinition sequenceDefinition;
MappingDefinition mappingDefinition;
OneOfDefinition oneOfDefinition;
// template-schema
mappingDefinition = new MappingDefinition();
mappingDefinition.Properties.Add(TemplateConstants.Version, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.NonEmptyString)));
mappingDefinition.Properties.Add(TemplateConstants.Definitions, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.Definitions)));
schema.Definitions.Add(TemplateConstants.TemplateSchema, mappingDefinition);
// definitions
mappingDefinition = new MappingDefinition();
mappingDefinition.LooseKeyType = TemplateConstants.NonEmptyString;
mappingDefinition.LooseValueType = TemplateConstants.Definition;
schema.Definitions.Add(TemplateConstants.Definitions, mappingDefinition);
// definition
oneOfDefinition = new OneOfDefinition();
oneOfDefinition.OneOf.Add(TemplateConstants.NullDefinition);
oneOfDefinition.OneOf.Add(TemplateConstants.BooleanDefinition);
oneOfDefinition.OneOf.Add(TemplateConstants.NumberDefinition);
oneOfDefinition.OneOf.Add(TemplateConstants.StringDefinition);
oneOfDefinition.OneOf.Add(TemplateConstants.SequenceDefinition);
oneOfDefinition.OneOf.Add(TemplateConstants.MappingDefinition);
oneOfDefinition.OneOf.Add(TemplateConstants.OneOfDefinition);
schema.Definitions.Add(TemplateConstants.Definition, oneOfDefinition);
// null-definition
mappingDefinition = new MappingDefinition();
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.String)));
mappingDefinition.Properties.Add(TemplateConstants.CoerceRaw, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.Boolean)));
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString)));
mappingDefinition.Properties.Add(TemplateConstants.Null, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.NullDefinitionProperties)));
schema.Definitions.Add(TemplateConstants.NullDefinition, mappingDefinition);
// null-definition-properties
mappingDefinition = new MappingDefinition();
schema.Definitions.Add(TemplateConstants.NullDefinitionProperties, mappingDefinition);
// boolean-definition
mappingDefinition = new MappingDefinition();
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.String)));
mappingDefinition.Properties.Add(TemplateConstants.CoerceRaw, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.Boolean)));
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString)));
mappingDefinition.Properties.Add(TemplateConstants.Boolean, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.BooleanDefinitionProperties)));
schema.Definitions.Add(TemplateConstants.BooleanDefinition, mappingDefinition);
// boolean-definition-properties
mappingDefinition = new MappingDefinition();
schema.Definitions.Add(TemplateConstants.BooleanDefinitionProperties, mappingDefinition);
// number-definition
mappingDefinition = new MappingDefinition();
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.String)));
mappingDefinition.Properties.Add(TemplateConstants.CoerceRaw, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.Boolean)));
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString)));
mappingDefinition.Properties.Add(TemplateConstants.Number, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.NumberDefinitionProperties)));
schema.Definitions.Add(TemplateConstants.NumberDefinition, mappingDefinition);
// number-definition-properties
mappingDefinition = new MappingDefinition();
schema.Definitions.Add(TemplateConstants.NumberDefinitionProperties, mappingDefinition);
// string-definition
mappingDefinition = new MappingDefinition();
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.String)));
mappingDefinition.Properties.Add(TemplateConstants.CoerceRaw, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.Boolean)));
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString)));
mappingDefinition.Properties.Add(TemplateConstants.String, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.StringDefinitionProperties)));
schema.Definitions.Add(TemplateConstants.StringDefinition, mappingDefinition);
// string-definition-properties
mappingDefinition = new MappingDefinition();
mappingDefinition.Properties.Add(TemplateConstants.Constant, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.NonEmptyString)));
mappingDefinition.Properties.Add(TemplateConstants.IgnoreCase, new PropertyDefinition(new StringToken(null, null, null,TemplateConstants.Boolean)));
mappingDefinition.Properties.Add(TemplateConstants.RequireNonEmpty, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.Boolean)));
mappingDefinition.Properties.Add(TemplateConstants.IsExpression, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.Boolean)));
schema.Definitions.Add(TemplateConstants.StringDefinitionProperties, mappingDefinition);
// sequence-definition
mappingDefinition = new MappingDefinition();
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.String)));
mappingDefinition.Properties.Add(TemplateConstants.CoerceRaw, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.Boolean)));
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString)));
mappingDefinition.Properties.Add(TemplateConstants.Sequence, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.SequenceDefinitionProperties)));
schema.Definitions.Add(TemplateConstants.SequenceDefinition, mappingDefinition);
// sequence-definition-properties
mappingDefinition = new MappingDefinition();
mappingDefinition.Properties.Add(TemplateConstants.ItemType, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.NonEmptyString)));
schema.Definitions.Add(TemplateConstants.SequenceDefinitionProperties, mappingDefinition);
// mapping-definition
mappingDefinition = new MappingDefinition();
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.String)));
mappingDefinition.Properties.Add(TemplateConstants.CoerceRaw, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.Boolean)));
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString)));
mappingDefinition.Properties.Add(TemplateConstants.Mapping, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.MappingDefinitionProperties)));
schema.Definitions.Add(TemplateConstants.MappingDefinition, mappingDefinition);
// mapping-definition-properties
mappingDefinition = new MappingDefinition();
mappingDefinition.Properties.Add(TemplateConstants.Properties, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.Properties)));
mappingDefinition.Properties.Add(TemplateConstants.LooseKeyType, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.NonEmptyString)));
mappingDefinition.Properties.Add(TemplateConstants.LooseValueType, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.NonEmptyString)));
schema.Definitions.Add(TemplateConstants.MappingDefinitionProperties, mappingDefinition);
// properties
mappingDefinition = new MappingDefinition();
mappingDefinition.LooseKeyType = TemplateConstants.NonEmptyString;
mappingDefinition.LooseValueType = TemplateConstants.PropertyValue;
schema.Definitions.Add(TemplateConstants.Properties, mappingDefinition);
// property-value
oneOfDefinition = new OneOfDefinition();
oneOfDefinition.OneOf.Add(TemplateConstants.NonEmptyString);
oneOfDefinition.OneOf.Add(TemplateConstants.MappingPropertyValue);
schema.Definitions.Add(TemplateConstants.PropertyValue, oneOfDefinition);
// mapping-property-value
mappingDefinition = new MappingDefinition();
mappingDefinition.Properties.Add(TemplateConstants.Type, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.NonEmptyString)));
mappingDefinition.Properties.Add(TemplateConstants.Required, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.Boolean)));
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.String)));
schema.Definitions.Add(TemplateConstants.MappingPropertyValue, mappingDefinition);
// one-of-definition
mappingDefinition = new MappingDefinition();
mappingDefinition.Properties.Add(TemplateConstants.Description, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.String)));
mappingDefinition.Properties.Add(TemplateConstants.CoerceRaw, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.Boolean)));
mappingDefinition.Properties.Add(TemplateConstants.Context, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString)));
mappingDefinition.Properties.Add(TemplateConstants.OneOf, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString)));
mappingDefinition.Properties.Add(TemplateConstants.AllowedValues, new PropertyDefinition(new StringToken(null, null, null, TemplateConstants.SequenceOfNonEmptyString)));
schema.Definitions.Add(TemplateConstants.OneOfDefinition, mappingDefinition);
// non-empty-string
stringDefinition = new StringDefinition();
stringDefinition.RequireNonEmpty = true;
schema.Definitions.Add(TemplateConstants.NonEmptyString, stringDefinition);
// sequence-of-non-empty-string
sequenceDefinition = new SequenceDefinition();
sequenceDefinition.ItemType = TemplateConstants.NonEmptyString;
schema.Definitions.Add(TemplateConstants.SequenceOfNonEmptyString, sequenceDefinition);
schema.Validate();
Interlocked.CompareExchange(ref s_schema, schema, null);
}
return s_schema;
}
}
private void Validate()
{
var oneOfPairs = new List<KeyValuePair<String, OneOfDefinition>>();
foreach (var pair in Definitions)
{
var name = pair.Key;
if (!s_definitionNameRegex.IsMatch(name ?? String.Empty))
{
throw new ArgumentException($"Invalid definition name '{name}'");
}
var definition = pair.Value;
// Delay validation for 'one-of' definitions
if (definition is OneOfDefinition oneOf)
{
oneOfPairs.Add(new KeyValuePair<String, OneOfDefinition>(name, oneOf));
}
// Otherwise validate now
else
{
definition.Validate(this, name);
}
}
// Validate 'one-of' definitions
foreach (var pair in oneOfPairs)
{
var name = pair.Key;
var oneOf = pair.Value;
oneOf.Validate(this, name);
}
}
private static readonly Regex s_definitionNameRegex = new Regex("^[a-zA-Z_][a-zA-Z0-9_-]*$", RegexOptions.Compiled);
private static TemplateSchema s_schema;
}
}
@@ -0,0 +1,20 @@
using System;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating
{
/// <summary>
/// Tracks telemetry data during workflow parsing.
/// </summary>
public sealed class Telemetry
{
/// <summary>
/// Gets or sets the count of YAML anchors encountered during parsing.
/// </summary>
public Int32 YamlAnchors { get; set; }
/// <summary>
/// Gets or sets the count of YAML aliases encountered during parsing.
/// </summary>
public Int32 YamlAliases { get; set; }
}
}
@@ -0,0 +1,58 @@
using System;
using System.Linq;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating
{
internal static class TemplateConstants
{
internal const String AllowedValues = "allowed-values";
internal const String Any = "any";
internal const String Boolean = "boolean";
internal const String BooleanDefinition = "boolean-definition";
internal const String BooleanDefinitionProperties = "boolean-definition-properties";
internal const String CloseExpression = "}}";
internal const String CoerceRaw = "coerce-raw";
internal const String Constant = "constant";
internal const String Context = "context";
internal const String Definition = "definition";
internal const String Definitions = "definitions";
internal const String Description = "description";
internal const String IgnoreCase = "ignore-case";
internal const String InsertDirective = "insert";
internal const String IsExpression = "is-expression";
internal const String ItemType = "item-type";
internal const String LooseKeyType = "loose-key-type";
internal const String LooseValueType = "loose-value-type";
internal const String MaxConstant = "MAX";
internal const String Mapping = "mapping";
internal const String MappingDefinition = "mapping-definition";
internal const String MappingDefinitionProperties = "mapping-definition-properties";
internal const String MappingPropertyValue = "mapping-property-value";
internal const String NonEmptyString = "non-empty-string";
internal const String Null = "null";
internal const String NullDefinition = "null-definition";
internal const String NullDefinitionProperties = "null-definition-properties";
internal const String Number = "number";
internal const String NumberDefinition = "number-definition";
internal const String NumberDefinitionProperties = "number-definition-properties";
internal const String OneOf = "one-of";
internal const String OneOfDefinition = "one-of-definition";
internal const String OpenExpression = "${{";
internal const String PropertyValue = "property-value";
internal const String Properties = "properties";
internal const String Required = "required";
internal const String RequireNonEmpty = "require-non-empty";
internal const String Scalar = "scalar";
internal const String Sequence = "sequence";
internal const String SequenceDefinition = "sequence-definition";
internal const String SequenceDefinitionProperties = "sequence-definition-properties";
internal const String Type = "type";
internal const String SequenceOfNonEmptyString = "sequence-of-non-empty-string";
internal const String String = "string";
internal const String StringDefinition = "string-definition";
internal const String StringDefinitionProperties = "string-definition-properties";
internal const String Structure = "structure";
internal const String TemplateSchema = "template-schema";
internal const String Version = "version";
}
}
@@ -0,0 +1,261 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using GitHub.Actions.Expressions;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Schema;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating
{
/// <summary>
/// Context object that is flowed through while loading and evaluating object templates
/// </summary>
public sealed class TemplateContext
{
internal CancellationToken CancellationToken { get; set; }
internal TemplateValidationErrors Errors
{
get
{
if (m_errors == null)
{
m_errors = new TemplateValidationErrors();
}
return m_errors;
}
set
{
m_errors = value;
}
}
/// <summary>
/// Available functions within expression contexts
/// </summary>
internal IList<IFunctionInfo> ExpressionFunctions
{
get
{
if (m_expressionFunctions == null)
{
m_expressionFunctions = new List<IFunctionInfo>();
}
return m_expressionFunctions;
}
}
/// <summary>
/// Available values within expression contexts
/// </summary>
internal IDictionary<String, Object> ExpressionValues
{
get
{
if (m_expressionValues == null)
{
m_expressionValues = new Dictionary<String, Object>(StringComparer.OrdinalIgnoreCase);
}
return m_expressionValues;
}
}
internal TemplateMemory Memory { get; set; }
internal TemplateSchema Schema { get; set; }
/// <summary>
/// Gets or sets the telemetry data collected during parsing.
/// </summary>
public Telemetry Telemetry
{
get
{
if (m_telemetry == null)
{
m_telemetry = new Telemetry();
}
return m_telemetry;
}
set
{
m_telemetry = value;
}
}
/// <summary>
/// State data for the current evaluation
/// </summary>
public IDictionary<String, Object> State
{
get
{
if (m_state == null)
{
m_state = new Dictionary<String, Object>(StringComparer.OrdinalIgnoreCase);
}
return m_state;
}
}
/// <summary>
/// Gets or sets a value indicating whether the expression function fromJson performs strict JSON parsing.
/// Used during evaluation only.
/// </summary>
internal Boolean StrictJsonParsing { get; set; }
internal ITraceWriter TraceWriter { get; set; }
private IDictionary<String, Int32> FileIds
{
get
{
if (m_fileIds == null)
{
m_fileIds = new Dictionary<String, Int32>(StringComparer.OrdinalIgnoreCase);
}
return m_fileIds;
}
set
{
m_fileIds = value;
}
}
private List<String> FileNames
{
get
{
if (m_fileNames == null)
{
m_fileNames = new List<String>();
}
return m_fileNames;
}
set
{
m_fileNames = value;
}
}
internal void Error(
TemplateToken value,
Exception ex)
{
Error(value?.FileId, value?.Line, value?.Column, ex);
}
internal void Error(
Int32? fileId,
Int32? line,
Int32? column,
Exception ex)
{
var prefix = GetErrorPrefix(fileId, line, column);
Errors.Add(prefix, ex);
TraceWriter.Error(prefix, ex);
}
internal void Error(
TemplateToken value,
String message)
{
Error(value?.FileId, value?.Line, value?.Column, message);
}
internal void Error(
Int32? fileId,
Int32? line,
Int32? column,
String message)
{
var prefix = GetErrorPrefix(fileId, line, column);
if (!String.IsNullOrEmpty(prefix))
{
message = $"{prefix} {message}";
}
Errors.Add(message);
TraceWriter.Error(message);
}
internal INamedValueInfo[] GetExpressionNamedValues()
{
if (m_expressionValues?.Count > 0)
{
return m_expressionValues.Keys.Select(x => new NamedValueInfo<ContextValueNode>(x)).ToArray();
}
return null;
}
internal Int32 GetFileId(String file)
{
if (!FileIds.TryGetValue(file, out Int32 id))
{
id = FileIds.Count + 1;
FileIds.Add(file, id);
FileNames.Add(file);
Memory.AddBytes(file);
}
return id;
}
internal String GetFileName(Int32 fileId)
{
return FileNames.Count >= fileId ? FileNames[fileId - 1] : null;
}
internal IReadOnlyList<String> GetFileTable()
{
return FileNames.AsReadOnly();
}
private String GetErrorPrefix(
Int32? fileId,
Int32? line,
Int32? column)
{
var fileName = fileId.HasValue ? GetFileName(fileId.Value) : null;
if (!String.IsNullOrEmpty(fileName))
{
if (line != null && column != null)
{
return $"{fileName} {TemplateStrings.LineColumn(line, column)}:";
}
else
{
return $"{fileName}:";
}
}
else if (line != null && column != null)
{
return $"{TemplateStrings.LineColumn(line, column)}:";
}
else
{
return String.Empty;
}
}
private TemplateValidationErrors m_errors;
private IList<IFunctionInfo> m_expressionFunctions;
private IDictionary<String, Object> m_expressionValues;
private IDictionary<String, Int32> m_fileIds;
private List<String> m_fileNames;
private IDictionary<String, Object> m_state;
private Telemetry m_telemetry;
}
}
@@ -0,0 +1,428 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using System.Collections.Generic;
using System.Linq;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Schema;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating
{
/// <summary>
/// Expands expression tokens where the allowed context is available now. The allowed context is defined
/// within the schema. The available context is based on the ExpressionValues registered in the TemplateContext.
/// </summary>
internal partial class TemplateEvaluator
{
private TemplateEvaluator(
TemplateContext context,
TemplateToken template,
Int32 removeBytes)
{
m_context = context;
m_schema = context.Schema;
m_unraveler = new TemplateUnraveler(context, template, removeBytes);
}
internal static TemplateToken Evaluate(
TemplateContext context,
String type,
TemplateToken template,
Int32 removeBytes,
Int32? fileId)
{
TemplateToken result;
var evaluator = new TemplateEvaluator(context, template, removeBytes);
try
{
var availableContext = new HashSet<String>(StringComparer.OrdinalIgnoreCase);
foreach (var key in context.ExpressionValues.Keys)
{
availableContext.Add(key);
}
foreach (var function in context.ExpressionFunctions)
{
availableContext.Add($"{function.Name}()");
}
var definitionInfo = new DefinitionInfo(context.Schema, type, availableContext);
result = evaluator.Evaluate(definitionInfo);
if (result != null)
{
evaluator.m_unraveler.ReadEnd();
}
}
catch (Exception ex)
{
context.Error(fileId, null, null, ex);
result = null;
}
return result;
}
private TemplateToken Evaluate(DefinitionInfo definition)
{
// Scalar
if (m_unraveler.AllowScalar(definition.Expand, out ScalarToken scalar))
{
if (scalar is LiteralToken literal)
{
Validate(ref literal, definition);
return literal;
}
else
{
return scalar;
}
}
// Sequence start
if (m_unraveler.AllowSequenceStart(definition.Expand, out SequenceToken sequence))
{
var sequenceDefinition = definition.Get<SequenceDefinition>().FirstOrDefault();
// Legal
if (sequenceDefinition != null)
{
var itemDefinition = new DefinitionInfo(definition, sequenceDefinition.ItemType);
// Add each item
while (!m_unraveler.AllowSequenceEnd(definition.Expand))
{
var item = Evaluate(itemDefinition);
sequence.Add(item);
}
}
// Illegal
else
{
// Error
m_context.Error(sequence, TemplateStrings.UnexpectedSequenceStart());
// Skip each item
while (!m_unraveler.AllowSequenceEnd(expand: false))
{
m_unraveler.SkipSequenceItem();
}
}
return sequence;
}
// Mapping
if (m_unraveler.AllowMappingStart(definition.Expand, out MappingToken mapping))
{
var mappingDefinitions = definition.Get<MappingDefinition>().ToList();
// Legal
if (mappingDefinitions.Count > 0)
{
if (mappingDefinitions.Count > 1 ||
m_schema.HasProperties(mappingDefinitions[0]) ||
String.IsNullOrEmpty(mappingDefinitions[0].LooseKeyType))
{
HandleMappingWithWellKnownProperties(definition, mappingDefinitions, mapping);
}
else
{
var keyDefinition = new DefinitionInfo(definition, mappingDefinitions[0].LooseKeyType);
var valueDefinition = new DefinitionInfo(definition, mappingDefinitions[0].LooseValueType);
HandleMappingWithAllLooseProperties(definition, keyDefinition, valueDefinition, mapping);
}
}
// Illegal
else
{
m_context.Error(mapping, TemplateStrings.UnexpectedMappingStart());
while (!m_unraveler.AllowMappingEnd(expand: false))
{
m_unraveler.SkipMappingKey();
m_unraveler.SkipMappingValue();
}
}
return mapping;
}
throw new ArgumentException(TemplateStrings.ExpectedScalarSequenceOrMapping());
}
private void HandleMappingWithWellKnownProperties(
DefinitionInfo definition,
List<MappingDefinition> mappingDefinitions,
MappingToken mapping)
{
// Check if loose properties are allowed
String looseKeyType = null;
String looseValueType = null;
DefinitionInfo? looseKeyDefinition = null;
DefinitionInfo? looseValueDefinition = null;
if (!String.IsNullOrEmpty(mappingDefinitions[0].LooseKeyType))
{
looseKeyType = mappingDefinitions[0].LooseKeyType;
looseValueType = mappingDefinitions[0].LooseValueType;
}
var keys = new HashSet<String>(StringComparer.OrdinalIgnoreCase);
var hasExpressionKey = false;
while (m_unraveler.AllowScalar(definition.Expand, out ScalarToken nextKeyScalar))
{
// Expression
if (nextKeyScalar is ExpressionToken)
{
hasExpressionKey = true;
var anyDefinition = new DefinitionInfo(definition, TemplateConstants.Any);
mapping.Add(nextKeyScalar, Evaluate(anyDefinition));
continue;
}
// Not a string, convert
if (!(nextKeyScalar is StringToken nextKey))
{
nextKey = new StringToken(nextKeyScalar.FileId, nextKeyScalar.Line, nextKeyScalar.Column, nextKeyScalar.ToString());
}
// Duplicate
if (!keys.Add(nextKey.Value))
{
m_context.Error(nextKey, TemplateStrings.ValueAlreadyDefined(nextKey.Value));
m_unraveler.SkipMappingValue();
continue;
}
// Well known
if (m_schema.TryMatchKey(mappingDefinitions, nextKey.Value, out String nextValueType))
{
var nextValueDefinition = new DefinitionInfo(definition, nextValueType);
var nextValue = Evaluate(nextValueDefinition);
mapping.Add(nextKey, nextValue);
continue;
}
// Loose
if (looseKeyType != null)
{
if (looseKeyDefinition == null)
{
looseKeyDefinition = new DefinitionInfo(definition, looseKeyType);
looseValueDefinition = new DefinitionInfo(definition, looseValueType);
}
Validate(nextKey, looseKeyDefinition.Value);
var nextValue = Evaluate(looseValueDefinition.Value);
mapping.Add(nextKey, nextValue);
continue;
}
// Error
m_context.Error(nextKey, TemplateStrings.UnexpectedValue(nextKey.Value));
m_unraveler.SkipMappingValue();
}
// Only one
if (mappingDefinitions.Count > 1)
{
var hitCount = new Dictionary<String, Int32>();
foreach (MappingDefinition mapdef in mappingDefinitions)
{
foreach (String key in mapdef.Properties.Keys)
{
if (!hitCount.TryGetValue(key, out Int32 value))
{
hitCount.Add(key, 1);
}
else
{
hitCount[key] = value + 1;
}
}
}
List<String> nonDuplicates = new List<String>();
foreach (String key in hitCount.Keys)
{
if (hitCount[key] == 1)
{
nonDuplicates.Add(key);
}
}
nonDuplicates.Sort();
String listToDeDuplicate = String.Join(", ", nonDuplicates);
m_context.Error(mapping, TemplateStrings.UnableToDetermineOneOf(listToDeDuplicate));
}
else if (mappingDefinitions.Count == 1 && !hasExpressionKey)
{
foreach (var property in mappingDefinitions[0].Properties)
{
if (property.Value.Required)
{
if (!keys.Contains(property.Key))
{
m_context.Error(mapping, $"Required property is missing: {property.Key}");
}
}
}
}
m_unraveler.ReadMappingEnd();
}
private void HandleMappingWithAllLooseProperties(
DefinitionInfo mappingDefinition,
DefinitionInfo keyDefinition,
DefinitionInfo valueDefinition,
MappingToken mapping)
{
var keys = new HashSet<String>(StringComparer.OrdinalIgnoreCase);
while (m_unraveler.AllowScalar(mappingDefinition.Expand, out ScalarToken nextKeyScalar))
{
// Expression
if (nextKeyScalar is ExpressionToken)
{
if (nextKeyScalar is BasicExpressionToken)
{
mapping.Add(nextKeyScalar, Evaluate(valueDefinition));
}
else
{
var anyDefinition = new DefinitionInfo(mappingDefinition, TemplateConstants.Any);
mapping.Add(nextKeyScalar, Evaluate(anyDefinition));
}
continue;
}
// Not a string
if (!(nextKeyScalar is StringToken nextKey))
{
nextKey = new StringToken(nextKeyScalar.FileId, nextKeyScalar.Line, nextKeyScalar.Column, nextKeyScalar.ToString());
}
// Duplicate
if (!keys.Add(nextKey.Value))
{
m_context.Error(nextKey, TemplateStrings.ValueAlreadyDefined(nextKey.Value));
m_unraveler.SkipMappingValue();
continue;
}
// Validate
Validate(nextKey, keyDefinition);
// Add the pair
var nextValue = Evaluate(valueDefinition);
mapping.Add(nextKey, nextValue);
}
m_unraveler.ReadMappingEnd();
}
private void Validate(
StringToken stringToken,
DefinitionInfo definition)
{
var literal = stringToken as LiteralToken;
Validate(ref literal, definition);
}
private void Validate(
ref LiteralToken literal,
DefinitionInfo definition)
{
// Legal
var literal2 = literal;
if (definition.Get<ScalarDefinition>().Any(x => x.IsMatch(literal2)))
{
return;
}
// Not a string, convert
if (literal.Type != TokenType.String)
{
var stringToken = new StringToken(literal.FileId, literal.Line, literal.Column, literal.ToString());
// Legal
if (definition.Get<StringDefinition>().Any(x => x.IsMatch(stringToken)))
{
literal = stringToken;
return;
}
}
// Illegal
m_context.Error(literal, TemplateStrings.UnexpectedValue(literal));
}
private struct DefinitionInfo
{
public DefinitionInfo(
TemplateSchema schema,
String name,
HashSet<String> availableContext)
{
m_schema = schema;
m_availableContext = availableContext;
// Lookup the definition
Definition = m_schema.GetDefinition(name);
// Determine whether to expand
m_allowedContext = Definition.EvaluatorContext;
if (Definition.EvaluatorContext.Length > 0)
{
Expand = m_availableContext.IsSupersetOf(m_allowedContext);
}
else
{
Expand = false;
}
}
public DefinitionInfo(
DefinitionInfo parent,
String name)
{
m_schema = parent.m_schema;
m_availableContext = parent.m_availableContext;
// Lookup the definition
Definition = m_schema.GetDefinition(name);
// Determine whether to expand
if (Definition.EvaluatorContext.Length > 0)
{
m_allowedContext = new HashSet<String>(parent.m_allowedContext.Concat(Definition.EvaluatorContext), StringComparer.OrdinalIgnoreCase).ToArray();
Expand = m_availableContext.IsSupersetOf(m_allowedContext);
}
else
{
m_allowedContext = parent.m_allowedContext;
Expand = parent.Expand;
}
}
public IEnumerable<T> Get<T>()
where T : Definition
{
return m_schema.Get<T>(Definition);
}
private HashSet<String> m_availableContext;
private String[] m_allowedContext;
private TemplateSchema m_schema;
public Definition Definition;
public Boolean Expand;
}
private readonly TemplateContext m_context;
private readonly TemplateSchema m_schema;
private readonly TemplateUnraveler m_unraveler;
}
}
@@ -0,0 +1,385 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using System.Collections.Generic;
using System.Linq;
using GitHub.Actions.Expressions.Data;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
using Newtonsoft.Json.Linq;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating
{
/// <summary>
/// Tracks characteristics about the current memory usage (CPU, stack, size)
/// </summary>
public sealed class TemplateMemory
{
/// <summary>
/// Creates a new instance
/// </summary>
/// <param name="maxBytes">The maximum allowed bytes</param>
public TemplateMemory(Int32 maxBytes)
: this(0, 0, maxBytes: maxBytes, null)
{
}
/// <summary>
/// Creates a new instance
/// </summary>
/// <param name="maxDepth">The maximum allowed depth</param>
/// <param name="maxEvents">The maximum allowed events</param>
/// <param name="maxBytes">The maximum allowed bytes</param>
internal TemplateMemory(
Int32 maxDepth,
Int32 maxEvents,
Int32 maxBytes)
: this(maxDepth, maxEvents, maxBytes, null)
{
}
/// <summary>
/// Creates a new instance
/// </summary>
/// <param name="maxDepth">The maximum allowed depth</param>
/// <param name="maxEvents">The maximum allowed events</param>
/// <param name="maxBytes">The maximum allowed bytes</param>
/// <param name="parent">Optional parent instance, for byte tracking only. Any bytes added/subtracted to the current instance, will be also added/subtracted to the parent instance.</param>
internal TemplateMemory(
Int32 maxDepth,
Int32 maxEvents,
Int32 maxBytes,
TemplateMemory parent)
{
m_maxDepth = maxDepth;
m_maxEvents = maxEvents;
m_maxBytes = maxBytes;
m_parent = parent;
}
public Int32 CurrentBytes => m_currentBytes;
public Int32 MaxBytes => m_maxBytes;
public void AddBytes(Int32 bytes)
{
checked
{
m_currentBytes += bytes;
}
if (m_currentBytes > m_maxBytes)
{
throw new InvalidOperationException(TemplateStrings.MaxObjectSizeExceeded());
}
m_parent?.AddBytes(bytes);
}
public void AddBytes(String value)
{
var bytes = CalculateBytes(value);
AddBytes(bytes);
}
internal void AddBytes(
ExpressionData value,
Boolean traverse)
{
var bytes = CalculateBytes(value, traverse);
AddBytes(bytes);
}
internal void AddBytes(
JToken value,
Boolean traverse)
{
var bytes = CalculateBytes(value, traverse);
AddBytes(bytes);
}
internal void AddBytes(
TemplateToken value,
Boolean traverse = false)
{
var bytes = CalculateBytes(value, traverse);
AddBytes(bytes);
}
internal void AddBytes(LiteralToken literal)
{
var bytes = CalculateBytes(literal);
AddBytes(bytes);
}
internal void AddBytes(SequenceToken sequence)
{
var bytes = CalculateBytes(sequence);
AddBytes(bytes);
}
internal void AddBytes(MappingToken mapping)
{
var bytes = CalculateBytes(mapping);
AddBytes(bytes);
}
internal void AddBytes(BasicExpressionToken basicExpression)
{
var bytes = CalculateBytes(basicExpression);
AddBytes(bytes);
}
internal void AddBytes(InsertExpressionToken insertExpression)
{
var bytes = CalculateBytes(insertExpression);
AddBytes(bytes);
}
internal Int32 CalculateBytes(String value)
{
// This measurement doesn't have to be perfect
// https://codeblog.jonskeet.uk/2011/04/05/of-memory-and-strings/
checked
{
return StringBaseOverhead + ((value?.Length ?? 0) * sizeof(Char));
}
}
internal static Int32 CalculateBytes(
ExpressionData value,
Boolean traverse)
{
var enumerable = traverse ? value.Traverse() : new[] { value } as IEnumerable<ExpressionData>;
var result = 0;
foreach (var item in enumerable)
{
// This measurement doesn't have to be perfect
// https://codeblog.jonskeet.uk/2011/04/05/of-memory-and-strings/
if (item is StringExpressionData str)
{
checked
{
result += TemplateMemory.MinObjectSize + TemplateMemory.StringBaseOverhead + (str.Value.Length * sizeof(Char));
}
}
else if (item is ArrayExpressionData || item is DictionaryExpressionData || item is BooleanExpressionData || item is NumberExpressionData)
{
// Min object size is good enough. Allows for base + a few fields.
checked
{
result += TemplateMemory.MinObjectSize;
}
}
else if (item is null)
{
checked
{
result += IntPtr.Size;
}
}
else
{
throw new NotSupportedException($"Unexpected workflow context data type '{item.GetType().Name}'");
}
}
return result;
}
internal Int32 CalculateBytes(
JToken value,
Boolean traverse)
{
// This measurement doesn't have to be perfect
// https://codeblog.jonskeet.uk/2011/04/05/of-memory-and-strings/
if (value is null)
{
return MinObjectSize;
}
if (!traverse)
{
switch (value.Type)
{
case JTokenType.String:
checked
{
return StringBaseOverhead + (value.ToObject<String>().Length * sizeof(Char));
}
case JTokenType.Property:
var property = value as JProperty;
checked
{
return StringBaseOverhead + ((property.Name?.Length ?? 0) * sizeof(Char));
}
case JTokenType.Array:
case JTokenType.Boolean:
case JTokenType.Float:
case JTokenType.Integer:
case JTokenType.Null:
case JTokenType.Object:
return MinObjectSize;
default:
throw new NotSupportedException($"Unexpected JToken type '{value.Type}' when traversing object");
}
}
var result = 0;
do
{
// Descend as much as possible
while (true)
{
// Add bytes
var bytes = CalculateBytes(value, false);
checked
{
result += bytes;
}
// Descend
if (value.HasValues)
{
value = value.First;
}
// No more descendants
else
{
break;
}
}
// Next sibling or ancestor sibling
do
{
var sibling = value.Next;
// Sibling found
if (sibling != null)
{
value = sibling;
break;
}
// Ascend
value = value.Parent;
} while (value != null);
} while (value != null);
return result;
}
internal Int32 CalculateBytes(
TemplateToken value,
Boolean traverse = false)
{
var enumerable = traverse ? value.Traverse() : new[] { value };
var result = 0;
foreach (var item in enumerable)
{
// This measurement doesn't have to be perfect
// https://codeblog.jonskeet.uk/2011/04/05/of-memory-and-strings/
switch (item.Type)
{
case TokenType.Null:
case TokenType.Boolean:
case TokenType.Number:
checked
{
result += MinObjectSize;
}
break;
case TokenType.String:
var stringToken = item as StringToken;
checked
{
result += MinObjectSize + StringBaseOverhead + ((stringToken.Value?.Length ?? 0) * sizeof(Char));
}
break;
case TokenType.Sequence:
case TokenType.Mapping:
case TokenType.InsertExpression:
// Min object size is good enough. Allows for base + a few fields.
checked
{
result += MinObjectSize;
}
break;
case TokenType.BasicExpression:
var basicExpression = item as BasicExpressionToken;
checked
{
result += MinObjectSize + StringBaseOverhead + ((basicExpression.Expression?.Length ?? 0) * sizeof(Char));
}
break;
default:
throw new NotSupportedException($"Unexpected template type '{item.Type}'");
}
}
return result;
}
internal void SubtractBytes(Int32 bytes)
{
if (bytes > m_currentBytes)
{
throw new InvalidOperationException("Bytes to subtract exceeds total bytes");
}
m_currentBytes -= bytes;
m_parent?.SubtractBytes(bytes);
}
internal void SubtractBytes(
TemplateToken value,
Boolean traverse = false)
{
var bytes = CalculateBytes(value, traverse);
SubtractBytes(bytes);
}
internal void IncrementDepth()
{
if (m_depth++ >= m_maxDepth)
{
throw new InvalidOperationException(TemplateStrings.MaxObjectDepthExceeded());
}
}
internal void DecrementDepth()
{
m_depth--;
}
internal void IncrementEvents()
{
if (m_events++ >= m_maxEvents)
{
throw new InvalidOperationException(TemplateStrings.MaxTemplateEventsExceeded());
}
}
internal const Int32 MinObjectSize = 24;
internal const Int32 StringBaseOverhead = 26;
private readonly Int32 m_maxDepth;
private readonly Int32 m_maxEvents;
private readonly Int32 m_maxBytes;
private Int32 m_depth;
private Int32 m_events;
private Int32 m_currentBytes;
private TemplateMemory m_parent;
}
}
@@ -0,0 +1,832 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using GitHub.Actions.Expressions.Sdk;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Schema;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating
{
/// <summary>
/// Converts a source object format into a TemplateToken
/// </summary>
internal sealed class TemplateReader
{
private TemplateReader(
TemplateContext context,
TemplateSchema schema,
IObjectReader objectReader,
Int32? fileId)
{
m_context = context;
m_schema = schema;
m_memory = context.Memory;
m_objectReader = objectReader;
m_fileId = fileId;
}
internal static TemplateToken Read(
TemplateContext context,
String type,
IObjectReader objectReader,
Int32? fileId,
out Int32 bytes)
{
return Read(context, type, objectReader, fileId, context.Schema, out bytes);
}
internal static TemplateToken Read(
TemplateContext context,
String type,
IObjectReader objectReader,
Int32? fileId,
TemplateSchema schema,
out Int32 bytes)
{
TemplateToken result = null;
var reader = new TemplateReader(context, schema, objectReader, fileId);
var originalBytes = context.Memory.CurrentBytes;
try
{
objectReader.ValidateStart();
var definition = new DefinitionInfo(schema, type);
result = reader.ReadValue(definition);
objectReader.ValidateEnd();
}
catch (Exception ex)
{
context.Error(fileId, null, null, ex);
}
finally
{
bytes = context.Memory.CurrentBytes - originalBytes;
}
return result;
}
private TemplateToken ReadValue(DefinitionInfo definition)
{
m_memory.IncrementEvents();
// Scalar
if (m_objectReader.AllowLiteral(out LiteralToken literal))
{
var scalar = ParseScalar(literal, definition.AllowedContext);
Validate(ref scalar, definition);
m_memory.AddBytes(scalar);
return scalar;
}
// Sequence
if (m_objectReader.AllowSequenceStart(out SequenceToken sequence))
{
m_memory.IncrementDepth();
m_memory.AddBytes(sequence);
var sequenceDefinition = definition.Get<SequenceDefinition>().FirstOrDefault();
// Legal
if (sequenceDefinition != null)
{
var itemDefinition = new DefinitionInfo(definition, sequenceDefinition.ItemType);
// Add each item
while (!m_objectReader.AllowSequenceEnd())
{
var item = ReadValue(itemDefinition);
sequence.Add(item);
}
}
// Illegal
else
{
// Error
m_context.Error(sequence, TemplateStrings.UnexpectedSequenceStart());
// Skip each item
while (!m_objectReader.AllowSequenceEnd())
{
SkipValue();
}
}
m_memory.DecrementDepth();
return sequence;
}
// Mapping
if (m_objectReader.AllowMappingStart(out MappingToken mapping))
{
m_memory.IncrementDepth();
m_memory.AddBytes(mapping);
var mappingDefinitions = definition.Get<MappingDefinition>().ToList();
// Legal
if (mappingDefinitions.Count > 0)
{
if (mappingDefinitions.Count > 1 ||
m_schema.HasProperties(mappingDefinitions[0]) ||
String.IsNullOrEmpty(mappingDefinitions[0].LooseKeyType))
{
HandleMappingWithWellKnownProperties(definition, mappingDefinitions, mapping);
}
else
{
var keyDefinition = new DefinitionInfo(definition, mappingDefinitions[0].LooseKeyType);
var valueDefinition = new DefinitionInfo(definition, mappingDefinitions[0].LooseValueType);
HandleMappingWithAllLooseProperties(definition, keyDefinition, valueDefinition, mapping);
}
}
// Illegal
else
{
m_context.Error(mapping, TemplateStrings.UnexpectedMappingStart());
while (!m_objectReader.AllowMappingEnd())
{
SkipValue();
SkipValue();
}
}
m_memory.DecrementDepth();
return mapping;
}
throw new InvalidOperationException(TemplateStrings.ExpectedScalarSequenceOrMapping());
}
private void HandleMappingWithWellKnownProperties(
DefinitionInfo definition,
List<MappingDefinition> mappingDefinitions,
MappingToken mapping)
{
// Check if loose properties are allowed
String looseKeyType = null;
String looseValueType = null;
DefinitionInfo? looseKeyDefinition = null;
DefinitionInfo? looseValueDefinition = null;
if (!String.IsNullOrEmpty(mappingDefinitions[0].LooseKeyType))
{
looseKeyType = mappingDefinitions[0].LooseKeyType;
looseValueType = mappingDefinitions[0].LooseValueType;
}
var keys = new HashSet<String>(StringComparer.OrdinalIgnoreCase);
var hasExpressionKey = false;
while (m_objectReader.AllowLiteral(out LiteralToken rawLiteral))
{
var nextKeyScalar = ParseScalar(rawLiteral, definition.AllowedContext);
// Expression
if (nextKeyScalar is ExpressionToken)
{
hasExpressionKey = true;
// Legal
if (definition.AllowedContext.Length > 0)
{
m_memory.AddBytes(nextKeyScalar);
var anyDefinition = new DefinitionInfo(definition, TemplateConstants.Any);
mapping.Add(nextKeyScalar, ReadValue(anyDefinition));
}
// Illegal
else
{
m_context.Error(nextKeyScalar, TemplateStrings.ExpressionNotAllowed());
SkipValue();
}
continue;
}
// Not a string, convert
if (!(nextKeyScalar is StringToken nextKey))
{
nextKey = new StringToken(nextKeyScalar.FileId, nextKeyScalar.Line, nextKeyScalar.Column, nextKeyScalar.ToString());
}
// Duplicate
if (!keys.Add(nextKey.Value))
{
m_context.Error(nextKey, TemplateStrings.ValueAlreadyDefined(nextKey.Value));
SkipValue();
continue;
}
// Well known
if (m_schema.TryMatchKey(mappingDefinitions, nextKey.Value, out String nextValueType))
{
m_memory.AddBytes(nextKey);
var nextValueDefinition = new DefinitionInfo(definition, nextValueType);
var nextValue = ReadValue(nextValueDefinition);
mapping.Add(nextKey, nextValue);
continue;
}
// Loose
if (looseKeyType != null)
{
if (looseKeyDefinition == null)
{
looseKeyDefinition = new DefinitionInfo(definition, looseKeyType);
looseValueDefinition = new DefinitionInfo(definition, looseValueType);
}
Validate(nextKey, looseKeyDefinition.Value);
m_memory.AddBytes(nextKey);
var nextValue = ReadValue(looseValueDefinition.Value);
mapping.Add(nextKey, nextValue);
continue;
}
// Error
m_context.Error(nextKey, TemplateStrings.UnexpectedValue(nextKey.Value));
SkipValue();
}
// Only one
if (mappingDefinitions.Count > 1)
{
var hitCount = new Dictionary<String, Int32>();
foreach (MappingDefinition mapdef in mappingDefinitions)
{
foreach (String key in mapdef.Properties.Keys)
{
if (!hitCount.TryGetValue(key, out Int32 value))
{
hitCount.Add(key, 1);
}
else
{
hitCount[key] = value + 1;
}
}
}
List<String> nonDuplicates = new List<String>();
foreach (String key in hitCount.Keys)
{
if(hitCount[key] == 1)
{
nonDuplicates.Add(key);
}
}
nonDuplicates.Sort();
String listToDeDuplicate = String.Join(", ", nonDuplicates);
m_context.Error(mapping, TemplateStrings.UnableToDetermineOneOf(listToDeDuplicate));
}
else if (mappingDefinitions.Count == 1 && !hasExpressionKey)
{
foreach (var property in mappingDefinitions[0].Properties)
{
if (property.Value.Required)
{
if (!keys.Contains(property.Key))
{
m_context.Error(mapping, $"Required property is missing: {property.Key}");
}
}
}
}
ExpectMappingEnd();
}
private void HandleMappingWithAllLooseProperties(
DefinitionInfo mappingDefinition,
DefinitionInfo keyDefinition,
DefinitionInfo valueDefinition,
MappingToken mapping)
{
TemplateToken nextValue;
var keys = new HashSet<String>(StringComparer.OrdinalIgnoreCase);
while (m_objectReader.AllowLiteral(out LiteralToken rawLiteral))
{
var nextKeyScalar = ParseScalar(rawLiteral, mappingDefinition.AllowedContext);
// Expression
if (nextKeyScalar is ExpressionToken)
{
// Legal
if (mappingDefinition.AllowedContext.Length > 0)
{
m_memory.AddBytes(nextKeyScalar);
nextValue = ReadValue(valueDefinition);
mapping.Add(nextKeyScalar, nextValue);
}
// Illegal
else
{
m_context.Error(nextKeyScalar, TemplateStrings.ExpressionNotAllowed());
SkipValue();
}
continue;
}
// Not a string, convert
if (!(nextKeyScalar is StringToken nextKey))
{
nextKey = new StringToken(nextKeyScalar.FileId, nextKeyScalar.Line, nextKeyScalar.Column, nextKeyScalar.ToString());
}
// Duplicate
if (!keys.Add(nextKey.Value))
{
m_context.Error(nextKey, TemplateStrings.ValueAlreadyDefined(nextKey.Value));
SkipValue();
continue;
}
// Validate
Validate(nextKey, keyDefinition);
m_memory.AddBytes(nextKey);
// Add the pair
nextValue = ReadValue(valueDefinition);
mapping.Add(nextKey, nextValue);
}
ExpectMappingEnd();
}
private void ExpectMappingEnd()
{
if (!m_objectReader.AllowMappingEnd())
{
throw new Exception("Expected mapping end"); // Should never happen
}
}
private void SkipValue(Boolean error = false)
{
m_memory.IncrementEvents();
// Scalar
if (m_objectReader.AllowLiteral(out LiteralToken literal))
{
if (error)
{
m_context.Error(literal, TemplateStrings.UnexpectedValue(literal));
}
return;
}
// Sequence
if (m_objectReader.AllowSequenceStart(out SequenceToken sequence))
{
m_memory.IncrementDepth();
if (error)
{
m_context.Error(sequence, TemplateStrings.UnexpectedSequenceStart());
}
while (!m_objectReader.AllowSequenceEnd())
{
SkipValue();
}
m_memory.DecrementDepth();
return;
}
// Mapping
if (m_objectReader.AllowMappingStart(out MappingToken mapping))
{
m_memory.IncrementDepth();
if (error)
{
m_context.Error(mapping, TemplateStrings.UnexpectedMappingStart());
}
while (!m_objectReader.AllowMappingEnd())
{
SkipValue();
SkipValue();
}
m_memory.DecrementDepth();
return;
}
// Unexpected
throw new InvalidOperationException(TemplateStrings.ExpectedScalarSequenceOrMapping());
}
private void Validate(
StringToken stringToken,
DefinitionInfo definition)
{
var scalar = stringToken as ScalarToken;
Validate(ref scalar, definition);
}
private void Validate(
ref ScalarToken scalar,
DefinitionInfo definition)
{
switch (scalar.Type)
{
case TokenType.Null:
case TokenType.Boolean:
case TokenType.Number:
case TokenType.String:
var literal = scalar as LiteralToken;
// Legal
if (definition.Get<ScalarDefinition>().Any(x => x.IsMatch(literal)))
{
return;
}
// Not a string, convert
if (literal.Type != TokenType.String)
{
literal = new StringToken(literal.FileId, literal.Line, literal.Column, literal.ToString());
// Legal
if (definition.Get<StringDefinition>().Any(x => x.IsMatch(literal)))
{
scalar = literal;
return;
}
}
// Illegal
m_context.Error(literal, TemplateStrings.UnexpectedValue(literal));
break;
case TokenType.BasicExpression:
// Illegal
if (definition.AllowedContext.Length == 0)
{
m_context.Error(scalar, TemplateStrings.ExpressionNotAllowed());
}
break;
default:
m_context.Error(scalar, TemplateStrings.UnexpectedValue(scalar));
break;
}
}
private ScalarToken ParseScalar(
LiteralToken token,
String[] allowedContext)
{
// Not a string
if (token.Type != TokenType.String)
{
return token;
}
// Check if the value is definitely a literal
var raw = token.ToString();
Int32 startExpression;
if (String.IsNullOrEmpty(raw) ||
(startExpression = raw.IndexOf(TemplateConstants.OpenExpression)) < 0) // Doesn't contain ${{
{
return token;
}
// Break the value into segments of LiteralToken and ExpressionToken
var segments = new List<ScalarToken>();
var i = 0;
while (i < raw.Length)
{
// An expression starts here:
if (i == startExpression)
{
// Find the end of the expression - i.e. }}
startExpression = i;
var endExpression = -1;
var inString = false;
for (i += TemplateConstants.OpenExpression.Length; i < raw.Length; i++)
{
if (raw[i] == '\'')
{
inString = !inString; // Note, this handles escaped single quotes gracefully. Ex. 'foo''bar'
}
else if (!inString && raw[i] == '}' && raw[i - 1] == '}')
{
endExpression = i;
i++;
break;
}
}
// Check if not closed
if (endExpression < startExpression)
{
m_context.Error(token, TemplateStrings.ExpressionNotClosed());
return token;
}
// Parse the expression
var rawExpression = raw.Substring(
startExpression + TemplateConstants.OpenExpression.Length,
endExpression - startExpression + 1 - TemplateConstants.OpenExpression.Length - TemplateConstants.CloseExpression.Length);
var expression = ParseExpression(token.Line, token.Column, rawExpression, allowedContext, out Exception ex);
// Check for error
if (ex != null)
{
m_context.Error(token, ex);
return token;
}
// Check if a directive was used when not allowed
if (!String.IsNullOrEmpty(expression.Directive) &&
((startExpression != 0) || (i < raw.Length)))
{
m_context.Error(token, TemplateStrings.DirectiveNotAllowedInline(expression.Directive));
return token;
}
// Add the segment
segments.Add(expression);
// Look for the next expression
startExpression = raw.IndexOf(TemplateConstants.OpenExpression, i);
}
// The next expression is further ahead:
else if (i < startExpression)
{
// Append the segment
AddString(segments, token.Line, token.Column, raw.Substring(i, startExpression - i));
// Adjust the position
i = startExpression;
}
// No remaining expressions:
else
{
AddString(segments, token.Line, token.Column, raw.Substring(i));
break;
}
}
// Check if can convert to a literal
// For example, the escaped expression: ${{ '{{ this is a literal }}' }}
if (segments.Count == 1 &&
segments[0] is BasicExpressionToken basicExpression &&
IsExpressionString(basicExpression.Expression, out String str))
{
return new StringToken(m_fileId, token.Line, token.Column, str);
}
// Check if only ony segment
if (segments.Count == 1)
{
return segments[0];
}
// Build the new expression, using the format function
var format = new StringBuilder();
var args = new StringBuilder();
var argIndex = 0;
foreach (var segment in segments)
{
if (segment is StringToken literal)
{
var text = ExpressionUtility.StringEscape(literal.Value) // Escape quotes
.Replace("{", "{{") // Escape braces
.Replace("}", "}}");
format.Append(text);
}
else
{
format.Append("{" + argIndex.ToString(CultureInfo.InvariantCulture) + "}"); // Append formatter
argIndex++;
var expression = segment as BasicExpressionToken;
args.Append(", ");
args.Append(expression.Expression);
}
}
var finalExpression = $"format('{format}'{args})";
if (!ExpressionToken.IsValidExpression(finalExpression, allowedContext, out Exception ex2))
{
m_context.Error(token, ex2);
return token;
}
return new BasicExpressionToken(m_fileId, token.Line, token.Column, finalExpression);
}
private ExpressionToken ParseExpression(
Int32? line,
Int32? column,
String value,
String[] allowedContext,
out Exception ex)
{
var trimmed = value.Trim();
// Check if the value is empty
if (String.IsNullOrEmpty(trimmed))
{
ex = new ArgumentException(TemplateStrings.ExpectedExpression());
return null;
}
// Try to find a matching directive
List<String> parameters;
if (MatchesDirective(trimmed, TemplateConstants.InsertDirective, 0, out parameters, out ex))
{
return new InsertExpressionToken(m_fileId, line, column);
}
else if (ex != null)
{
return null;
}
// Check if the value is an expression
if (!ExpressionToken.IsValidExpression(trimmed, allowedContext, out ex))
{
return null;
}
// Return the expression
return new BasicExpressionToken(m_fileId, line, column, trimmed);
}
private void AddString(
List<ScalarToken> segments,
Int32? line,
Int32? column,
String value)
{
// If the last segment was a LiteralToken, then append to the last segment
if (segments.Count > 0 && segments[segments.Count - 1] is StringToken lastSegment)
{
segments[segments.Count - 1] = new StringToken(m_fileId, line, column, lastSegment.Value + value);
}
// Otherwise add a new LiteralToken
else
{
segments.Add(new StringToken(m_fileId, line, column, value));
}
}
private static Boolean MatchesDirective(
String trimmed,
String directive,
Int32 expectedParameters,
out List<String> parameters,
out Exception ex)
{
if (trimmed.StartsWith(directive, StringComparison.Ordinal) &&
(trimmed.Length == directive.Length || Char.IsWhiteSpace(trimmed[directive.Length])))
{
parameters = new List<String>();
var startIndex = directive.Length;
var inString = false;
var parens = 0;
for (var i = startIndex; i < trimmed.Length; i++)
{
var c = trimmed[i];
if (Char.IsWhiteSpace(c) && !inString && parens == 0)
{
if (startIndex < i)
{
parameters.Add(trimmed.Substring(startIndex, i - startIndex));
}
startIndex = i + 1;
}
else if (c == '\'')
{
inString = !inString;
}
else if (c == '(' && !inString)
{
parens++;
}
else if (c == ')' && !inString)
{
parens--;
}
}
if (startIndex < trimmed.Length)
{
parameters.Add(trimmed.Substring(startIndex));
}
if (expectedParameters != parameters.Count)
{
ex = new ArgumentException(TemplateStrings.ExpectedNParametersFollowingDirective(expectedParameters, directive, parameters.Count));
parameters = null;
return false;
}
ex = null;
return true;
}
ex = null;
parameters = null;
return false;
}
private static Boolean IsExpressionString(
String trimmed,
out String str)
{
var builder = new StringBuilder();
var inString = false;
for (var i = 0; i < trimmed.Length; i++)
{
var c = trimmed[i];
if (c == '\'')
{
inString = !inString;
if (inString && i != 0)
{
builder.Append(c);
}
}
else if (!inString)
{
str = default;
return false;
}
else
{
builder.Append(c);
}
}
str = builder.ToString();
return true;
}
private struct DefinitionInfo
{
public DefinitionInfo(
TemplateSchema schema,
String name)
{
m_schema = schema;
// Lookup the definition
Definition = m_schema.GetDefinition(name);
// Record allowed context
AllowedContext = Definition.ReaderContext;
}
public DefinitionInfo(
DefinitionInfo parent,
String name)
{
m_schema = parent.m_schema;
// Lookup the definition
Definition = m_schema.GetDefinition(name);
// Record allowed context
if (Definition.ReaderContext.Length > 0)
{
AllowedContext = new HashSet<String>(parent.AllowedContext.Concat(Definition.ReaderContext), StringComparer.OrdinalIgnoreCase).ToArray();
}
else
{
AllowedContext = parent.AllowedContext;
}
}
public IEnumerable<T> Get<T>()
where T : Definition
{
return m_schema.Get<T>(Definition);
}
private TemplateSchema m_schema;
public Definition Definition;
public String[] AllowedContext;
}
private readonly TemplateContext m_context;
private readonly Int32? m_fileId;
private readonly TemplateMemory m_memory;
private readonly IObjectReader m_objectReader;
private readonly TemplateSchema m_schema;
}
}
@@ -0,0 +1,281 @@
// <auto-generated/>
// *** AUTOMATICALLY GENERATED BY GenResourceClass -- DO NOT EDIT!!! ***
using System;
using System.Diagnostics;
using System.ComponentModel;
using System.Globalization;
using System.Reflection;
using System.Resources;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating {
internal static class TemplateStrings
{
//********************************************************************************************
/// Creates the resource manager instance.
//********************************************************************************************
static TemplateStrings()
{
s_resMgr = new ResourceManager("GitHub.Actions.WorkflowParser.ObjectTemplating.TemplateStrings", typeof(TemplateStrings).GetTypeInfo().Assembly);
}
public static ResourceManager Manager
{
get
{
return s_resMgr;
}
}
//********************************************************************************************
/// Returns a localized string given a resource string name.
//********************************************************************************************
public static String Get(
String resourceName)
{
return s_resMgr.GetString(resourceName, CultureInfo.CurrentUICulture);
}
//********************************************************************************************
/// Returns a localized integer given a resource string name.
//********************************************************************************************
public static int GetInt(
String resourceName)
{
return (int)s_resMgr.GetObject(resourceName, CultureInfo.CurrentUICulture);
}
//********************************************************************************************
/// Returns a localized string given a resource string name.
//********************************************************************************************
public static bool GetBool(
String resourceName)
{
return (bool)s_resMgr.GetObject(resourceName, CultureInfo.CurrentUICulture);
}
//********************************************************************************************
/// A little helper function to alleviate some typing associated with loading resources and
/// formatting the strings. In DEBUG builds, it also asserts that the number of format
/// arguments and the length of args match.
//********************************************************************************************
private static String Format( // The formatted resource string.
String resourceName, // The name of the resource.
params Object[] args) // Arguments to format.
{
String resource = Get(resourceName);
#if DEBUG
// Check to make sure that the number of format string arguments matches the number of
// arguments passed in.
int formatArgCount = 0;
bool[] argSeen = new bool[100];
for (int i = 0; i < resource.Length; i++)
{
if (resource[i] == '{')
{
if (i + 1 < resource.Length &&
resource[i + 1] == '{')
{
i++; // Skip the escaped curly braces.
}
else
{
// Move past the curly brace and leading whitespace.
i++;
while (Char.IsWhiteSpace(resource[i]))
{
i++;
}
// Get the argument number.
int length = 0;
while (i + length < resource.Length && Char.IsDigit(resource[i + length]))
{
length++;
}
// Record it if it hasn't already been seen.
int argNumber = int.Parse(resource.Substring(i, length), CultureInfo.InvariantCulture);
if (!argSeen[argNumber])
{
formatArgCount++; // Count it as a formatting argument.
argSeen[argNumber] = true;
}
}
}
}
Debug.Assert(args != null || formatArgCount == 0,
String.Format(CultureInfo.InvariantCulture, "The number of format arguments is {0}, but the args parameter is null.", formatArgCount));
Debug.Assert(args == null || formatArgCount == args.Length,
String.Format(CultureInfo.InvariantCulture, "Coding error using resource \"{0}\": The number of format arguments {1} != number of args {2}",
resourceName, formatArgCount, args != null ? args.Length : 0));
#endif // DEBUG
if (args == null)
{
return resource;
}
// If there are any DateTime structs in the arguments, we need to bracket them
// to make sure they are within the supported range of the current calendar.
for (int i = 0; i < args.Length; i++)
{
// DateTime is a struct, we cannot use the as operator and null check.
if (args[i] is DateTime)
{
DateTime dateTime = (DateTime)args[i];
// We need to fetch the calendar on each Format call since it may change.
// Since we don't have more than one DateTime for resource, do not
// bother to cache this for the duration of the for loop.
Calendar calendar = DateTimeFormatInfo.CurrentInfo.Calendar;
if (dateTime > calendar.MaxSupportedDateTime)
{
args[i] = calendar.MaxSupportedDateTime;
}
else if (dateTime < calendar.MinSupportedDateTime)
{
args[i] = calendar.MinSupportedDateTime;
}
}
}
return String.Format(CultureInfo.CurrentCulture, resource, args);
}
// According to the documentation for the ResourceManager class, it should be sufficient to
// create a single static instance. The following is an excerpt from the 1.1 documentation.
// Using the methods of ResourceManager, a caller can access the resources for a particular
// culture using the GetObject and GetString methods. By default, these methods return the
// resource for the culture determined by the current cultural settings of the thread that made
// the call.
private static ResourceManager s_resMgr;
/// <summary>
/// The expression directive &apos;{0}&apos; is not supported in this context
/// </summary>
public static String DirectiveNotAllowed(object arg0) { return Format("DirectiveNotAllowed", arg0); }
/// <summary>
/// The directive &apos;{0}&apos; is not allowed in this context. Directives are not supported for expressions that are embedded within a string. Directives are only supported when the entire value is an expression.
/// </summary>
public static String DirectiveNotAllowedInline(object arg0) { return Format("DirectiveNotAllowedInline", arg0); }
/// <summary>
/// An expression was expected
/// </summary>
public static String ExpectedExpression() { return Get("ExpectedExpression"); }
/// <summary>
/// Expected a mapping
/// </summary>
public static String ExpectedMapping() { return Get("ExpectedMapping"); }
/// <summary>
/// Exactly {0} parameter(s) were expected following the directive &apos;{1}&apos;. Actual parameter count: {2}
/// </summary>
public static String ExpectedNParametersFollowingDirective(object arg0, object arg1, object arg2) { return Format("ExpectedNParametersFollowingDirective", arg0, arg1, arg2); }
/// <summary>
/// Expected a scalar value
/// </summary>
public static String ExpectedScalar() { return Get("ExpectedScalar"); }
/// <summary>
/// Expected a scalar value, a sequence, or a mapping
/// </summary>
public static String ExpectedScalarSequenceOrMapping() { return Get("ExpectedScalarSequenceOrMapping"); }
/// <summary>
/// Expected a sequence
/// </summary>
public static String ExpectedSequence() { return Get("ExpectedSequence"); }
/// <summary>
/// A template expression is not allowed in this context
/// </summary>
public static String ExpressionNotAllowed() { return Get("ExpressionNotAllowed"); }
/// <summary>
/// The expression is not closed. An unescaped ${{ sequence was found, but the closing }} sequence was not found.
/// </summary>
public static String ExpressionNotClosed() { return Get("ExpressionNotClosed"); }
/// <summary>
/// Error message prefix that indicates the line and column where the error occurred
///
/// (Line: {0}, Col: {1})
/// </summary>
public static String LineColumn(object arg0, object arg1) { return Format("LineColumn", arg0, arg1); }
/// <summary>
/// Maximum object depth exceeded
/// </summary>
public static String MaxObjectDepthExceeded() { return Get("MaxObjectDepthExceeded"); }
/// <summary>
/// Maximum object size exceeded
/// </summary>
public static String MaxObjectSizeExceeded() { return Get("MaxObjectSizeExceeded"); }
/// <summary>
/// Maximum events exceeded while evaluating the template. This may indicate an infinite loop or too many nested loops.
/// </summary>
public static String MaxTemplateEventsExceeded() { return Get("MaxTemplateEventsExceeded"); }
/// <summary>
/// The template is not valid.
/// </summary>
public static String TemplateNotValid() { return Get("TemplateNotValid"); }
/// <summary>
/// The template is not valid. {0}
/// </summary>
public static String TemplateNotValidWithErrors(object arg0) { return Format("TemplateNotValidWithErrors", arg0); }
/// <summary>
/// In {0} {1}: Error from called workflow
/// </summary>
public static String CalledWorkflowNotValidWithErrors(object arg0, object arg1) { return Format("CalledWorkflowNotValidWithErrors", arg0, arg1); }
/// <summary>
/// Unable to convert the object to a template token. Actual type &apos;{0}&apos;
/// </summary>
public static String UnableToConvertToTemplateToken(object arg0) { return Format("UnableToConvertToTemplateToken", arg0); }
/// <summary>
/// There&apos;s not enough info to determine what you meant. Add one of these properties: {0}
/// </summary>
public static String UnableToDetermineOneOf(object arg0) { return Format("UnableToDetermineOneOf", arg0); }
/// <summary>
/// A mapping was not expected
/// </summary>
public static String UnexpectedMappingStart() { return Get("UnexpectedMappingStart"); }
/// <summary>
/// A sequence was not expected
/// </summary>
public static String UnexpectedSequenceStart() { return Get("UnexpectedSequenceStart"); }
/// <summary>
/// Unexpected value &apos;{0}&apos;
/// </summary>
public static String UnexpectedValue(object arg0) { return Format("UnexpectedValue", arg0); }
/// <summary>
/// &apos;{0}&apos; is already defined
/// </summary>
public static String ValueAlreadyDefined(object arg0) { return Format("ValueAlreadyDefined", arg0); }
}
} // namespace
@@ -0,0 +1,190 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="DirectiveNotAllowed" xml:space="preserve">
<value>The expression directive '{0}' is not supported in this context</value>
</data>
<data name="DirectiveNotAllowedInline" xml:space="preserve">
<value>The directive '{0}' is not allowed in this context. Directives are not supported for expressions that are embedded within a string. Directives are only supported when the entire value is an expression.</value>
</data>
<data name="ExpectedExpression" xml:space="preserve">
<value>An expression was expected</value>
</data>
<data name="ExpectedMapping" xml:space="preserve">
<value>Expected a mapping</value>
</data>
<data name="ExpectedNParametersFollowingDirective" xml:space="preserve">
<value>Exactly {0} parameter(s) were expected following the directive '{1}'. Actual parameter count: {2}</value>
</data>
<data name="ExpectedScalar" xml:space="preserve">
<value>Expected a scalar value</value>
</data>
<data name="ExpectedScalarSequenceOrMapping" xml:space="preserve">
<value>Expected a scalar value, a sequence, or a mapping</value>
</data>
<data name="ExpectedSequence" xml:space="preserve">
<value>Expected a sequence</value>
</data>
<data name="ExpressionNotAllowed" xml:space="preserve">
<value>A template expression is not allowed in this context</value>
</data>
<data name="ExpressionNotClosed" xml:space="preserve">
<value>The expression is not closed. An unescaped ${{ sequence was found, but the closing }} sequence was not found.</value>
</data>
<data name="LineColumn" xml:space="preserve">
<value>(Line: {0}, Col: {1})</value>
<comment>Error message prefix that indicates the line and column where the error occurred</comment>
</data>
<data name="MaxObjectDepthExceeded" xml:space="preserve">
<value>Maximum object depth exceeded</value>
</data>
<data name="MaxObjectSizeExceeded" xml:space="preserve">
<value>Maximum object size exceeded</value>
</data>
<data name="MaxTemplateEventsExceeded" xml:space="preserve">
<value>Maximum events exceeded while evaluating the template. This may indicate an infinite loop or too many nested loops.</value>
</data>
<data name="TemplateNotValid" xml:space="preserve">
<value>The template is not valid.</value>
</data>
<data name="TemplateNotValidWithErrors" xml:space="preserve">
<value>The template is not valid. {0}</value>
</data>
<data name="CalledWorkflowNotValidWithErrors" xml:space="preserve">
<value>In {0} {1}: Error from called workflow</value>
</data>
<data name="UnableToConvertToTemplateToken" xml:space="preserve">
<value>Unable to convert the object to a template token. Actual type '{0}'</value>
</data>
<data name="UnableToDetermineOneOf" xml:space="preserve">
<value>There's not enough info to determine what you meant. Add one of these properties: {0}</value>
</data>
<data name="UnexpectedMappingStart" xml:space="preserve">
<value>A mapping was not expected</value>
</data>
<data name="UnexpectedSequenceStart" xml:space="preserve">
<value>A sequence was not expected</value>
</data>
<data name="UnexpectedValue" xml:space="preserve">
<value>Unexpected value '{0}'</value>
</data>
<data name="ValueAlreadyDefined" xml:space="preserve">
<value>'{0}' is already defined</value>
</data>
</root>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,60 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating
{
/// <summary>
/// Provides information about an error which occurred during validation.
/// </summary>
[DataContract]
public class TemplateValidationError
{
public TemplateValidationError()
{
}
public TemplateValidationError(String message)
: this(null, message)
{
}
public TemplateValidationError(
String code,
String message)
{
Code = code;
Message = message;
}
[DataMember(Name = "code", EmitDefaultValue = false)]
public String Code
{
get;
set;
}
[DataMember(Name = "Message", EmitDefaultValue = false)]
public String Message
{
get;
set;
}
public static IEnumerable<TemplateValidationError> Create(Exception exception)
{
for (int i = 0; i < 50; i++)
{
yield return new TemplateValidationError(exception.Message);
if (exception.InnerException == null)
{
break;
}
exception = exception.InnerException;
}
}
}
}
@@ -0,0 +1,157 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating
{
/// <summary>
/// Provides information about an error which occurred during validation.
/// </summary>
[DataContract]
public sealed class TemplateValidationErrors : IEnumerable<TemplateValidationError>
{
public TemplateValidationErrors()
{
}
public TemplateValidationErrors(
Int32 maxErrors,
Int32 maxMessageLength)
{
m_maxErrors = maxErrors;
m_maxMessageLength = maxMessageLength;
}
public Int32 Count => m_errors.Count;
public void Add(String message)
{
Add(new TemplateValidationError(message));
}
public void Add(Exception ex)
{
Add(null, ex);
}
public void Add(String messagePrefix, Exception ex)
{
for (int i = 0; i < 50; i++)
{
String message = !String.IsNullOrEmpty(messagePrefix) ? $"{messagePrefix} {ex.Message}" : ex.Message;
Add(new TemplateValidationError(message));
if (ex.InnerException == null)
{
break;
}
ex = ex.InnerException;
}
}
public void Add(IEnumerable<TemplateValidationError> errors)
{
foreach (var error in errors)
{
Add(error);
}
}
public void Add(TemplateValidationError error)
{
// Check max errors
if (m_maxErrors <= 0 ||
m_errors.Count < m_maxErrors)
{
// Check max message length
if (m_maxMessageLength > 0 &&
error.Message?.Length > m_maxMessageLength)
{
error = new TemplateValidationError(error.Code, error.Message.Substring(0, m_maxMessageLength) + "[...]");
}
m_errors.Add(error);
}
}
/// <summary>
/// Add a prefix in the error message of the given index.
/// </summary>
public void PrefixMessage(int index, String prefix) {
if (index < 0 || index >= m_errors.Count) {
return;
}
var message = GetMessage(index);
if (!String.IsNullOrEmpty(message))
{
m_errors[index].Message = $"{prefix} {message}";
}
else
{
m_errors[index].Message = $"{prefix}";
}
}
public string GetMessage(int index)
{
if (index < 0 || index >= m_errors.Count)
{
return null;
}
return m_errors[index].Message;
}
/// <summary>
/// Throws <c ref="TemplateValidationException" /> if any errors.
/// </summary>
public void Check()
{
if (m_errors.Count > 0)
{
throw new TemplateValidationException(m_errors);
}
}
/// <summary>
/// Throws <c ref="TemplateValidationException" /> if any errors.
/// <param name="prefix">The error message prefix</param>
/// </summary>
public void Check(String prefix)
{
if (String.IsNullOrEmpty(prefix))
{
this.Check();
}
else if (m_errors.Count > 0)
{
var message = $"{prefix.Trim()} {String.Join(",", m_errors.Select(e => e.Message))}";
throw new TemplateValidationException(message, m_errors);
}
}
public void Clear()
{
m_errors.Clear();
}
public IEnumerator<TemplateValidationError> GetEnumerator()
{
return (m_errors as IEnumerable<TemplateValidationError>).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return (m_errors as IEnumerable).GetEnumerator();
}
private readonly List<TemplateValidationError> m_errors = new List<TemplateValidationError>();
private readonly Int32 m_maxErrors;
private readonly Int32 m_maxMessageLength;
}
}
@@ -0,0 +1,57 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating
{
public class TemplateValidationException : Exception
{
public TemplateValidationException()
: this(TemplateStrings.TemplateNotValid())
{
}
public TemplateValidationException(IEnumerable<TemplateValidationError> errors)
: this(TemplateStrings.TemplateNotValidWithErrors(string.Join(",", (errors ?? Enumerable.Empty<TemplateValidationError>()).Select(e => e.Message))))
{
m_errors = new List<TemplateValidationError>(errors ?? Enumerable.Empty<TemplateValidationError>());
}
public TemplateValidationException(
String message,
IEnumerable<TemplateValidationError> errors)
: this(message)
{
m_errors = new List<TemplateValidationError>(errors ?? Enumerable.Empty<TemplateValidationError>());
}
public TemplateValidationException(String message)
: base(message)
{
}
public TemplateValidationException(
String message,
Exception innerException)
: base(message, innerException)
{
}
public IList<TemplateValidationError> Errors
{
get
{
if (m_errors == null)
{
m_errors = new List<TemplateValidationError>();
}
return m_errors;
}
}
private List<TemplateValidationError> m_errors;
}
}
@@ -0,0 +1,74 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating
{
/// <summary>
/// Converts from a TemplateToken into another object format
/// </summary>
internal sealed class TemplateWriter
{
internal static void Write(
IObjectWriter objectWriter,
TemplateToken value)
{
objectWriter.WriteStart();
WriteValue(objectWriter, value);
objectWriter.WriteEnd();
}
private static void WriteValue(
IObjectWriter objectWriter,
TemplateToken value)
{
switch (value?.Type ?? TokenType.Null)
{
case TokenType.Null:
objectWriter.WriteNull();
break;
case TokenType.Boolean:
var booleanToken = value as BooleanToken;
objectWriter.WriteBoolean(booleanToken.Value);
break;
case TokenType.Number:
var numberToken = value as NumberToken;
objectWriter.WriteNumber(numberToken.Value);
break;
case TokenType.String:
case TokenType.BasicExpression:
case TokenType.InsertExpression:
objectWriter.WriteString(value.ToString());
break;
case TokenType.Mapping:
var mappingToken = value as MappingToken;
objectWriter.WriteMappingStart();
foreach (var pair in mappingToken)
{
WriteValue(objectWriter, pair.Key);
WriteValue(objectWriter, pair.Value);
}
objectWriter.WriteMappingEnd();
break;
case TokenType.Sequence:
var sequenceToken = value as SequenceToken;
objectWriter.WriteSequenceStart();
foreach (var item in sequenceToken)
{
WriteValue(objectWriter, item);
}
objectWriter.WriteSequenceEnd();
break;
default:
throw new NotSupportedException($"Unexpected type '{value.GetType()}'");
}
}
}
}
@@ -0,0 +1,142 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using System.Globalization;
using System.Linq;
using System.Runtime.Serialization;
using GitHub.Actions.Expressions;
using GitHub.Actions.Expressions.Sdk;
using GitHub.Actions.Expressions.Sdk.Functions;
using Container = GitHub.Actions.Expressions.Sdk.Container;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens
{
[DataContract]
public sealed class BasicExpressionToken : ExpressionToken
{
public BasicExpressionToken(
Int32? fileId,
Int32? line,
Int32? column,
String expression)
: base(TokenType.BasicExpression, fileId, line, column, null)
{
m_expression = expression;
}
internal String Expression
{
get
{
if (m_expression == null)
{
m_expression = String.Empty;
}
return m_expression;
}
}
public override TemplateToken Clone(Boolean omitSource)
{
return omitSource ? new BasicExpressionToken(null, null, null, m_expression) : new BasicExpressionToken(FileId, Line, Column, m_expression);
}
public override String ToString()
{
return $"{TemplateConstants.OpenExpression} {m_expression} {TemplateConstants.CloseExpression}";
}
public override String ToDisplayString()
{
var expressionParser = new ExpressionParser();
var expressionNode = expressionParser.ValidateSyntax(Expression, null);
if (expressionNode is Format formatNode)
{
// Make sure our first item is indeed a literal string so we can format it.
if (formatNode.Parameters.Count > 1 &&
formatNode.Parameters.First() is Literal literalValueNode &&
literalValueNode.Kind == ValueKind.String)
{
// Get all other Parameters san the formatted string to pass into the formatter
var formatParameters = formatNode.Parameters.Skip(1).Select(x => this.ConvertFormatParameterToExpression(x)).ToArray();
if (formatParameters.Length > 0)
{
String formattedString = String.Empty;
try
{
formattedString = String.Format(CultureInfo.InvariantCulture, (formatNode.Parameters[0] as Literal).Value as String, formatParameters);
}
catch (FormatException) { }
catch (ArgumentNullException) { } // If this operation fails, revert to default display name
if (!String.IsNullOrEmpty(formattedString))
{
return TrimDisplayString(formattedString);
}
}
}
}
return base.ToDisplayString();
}
internal StringToken EvaluateStringToken(
TemplateContext context,
out Int32 bytes)
{
return EvaluateStringToken(context, Expression, out bytes);
}
internal MappingToken EvaluateMappingToken(
TemplateContext context,
out Int32 bytes)
{
return EvaluateMappingToken(context, Expression, out bytes);
}
internal SequenceToken EvaluateSequenceToken(
TemplateContext context,
out Int32 bytes)
{
return EvaluateSequenceToken(context, Expression, out bytes);
}
internal TemplateToken EvaluateTemplateToken(
TemplateContext context,
out Int32 bytes)
{
return EvaluateTemplateToken(context, Expression, out bytes);
}
[OnSerializing]
private void OnSerializing(StreamingContext context)
{
if (m_expression?.Length == 0)
{
m_expression = null;
}
}
private String ConvertFormatParameterToExpression(ExpressionNode node)
{
var nodeString = node.ConvertToExpression();
// If the node is a container, see if it starts with '(' and ends with ')' so we can simplify the string
// Should only simplify if only one '(' or ')' exists in the string
// We are trying to simplify the case (a || b) to a || b
// But we should avoid simplifying ( a && b
if (node is Container &&
nodeString.Length > 2 &&
nodeString[0] == ExpressionConstants.StartParameter &&
nodeString[nodeString.Length - 1] == ExpressionConstants.EndParameter &&
nodeString.Count(character => character == ExpressionConstants.StartParameter) == 1 &&
nodeString.Count(character => character == ExpressionConstants.EndParameter) == 1)
{
nodeString = nodeString = nodeString.Substring(1, nodeString.Length - 2);
}
return String.Concat(TemplateConstants.OpenExpression, " ", nodeString, " ", TemplateConstants.CloseExpression);
}
[DataMember(Name = "expr", EmitDefaultValue = false)]
private String m_expression;
}
}
@@ -0,0 +1,40 @@
using System;
using System.Runtime.Serialization;
using GitHub.Actions.Expressions.Sdk;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens
{
[DataContract]
public sealed class BooleanToken : LiteralToken, IBoolean
{
public BooleanToken(
Int32? fileId,
Int32? line,
Int32? column,
Boolean value)
: base(TokenType.Boolean, fileId, line, column)
{
m_value = value;
}
public Boolean Value => m_value;
public override TemplateToken Clone(Boolean omitSource)
{
return omitSource ? new BooleanToken(null, null, null, m_value) : new BooleanToken(FileId, Line, Column, m_value);
}
public override String ToString()
{
return m_value ? "true" : "false";
}
Boolean IBoolean.GetBoolean()
{
return Value;
}
[DataMember(Name = "bool", EmitDefaultValue = false)]
private Boolean m_value;
}
}
@@ -0,0 +1,95 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Runtime.Serialization;
using System.Text.RegularExpressions;
using GitHub.Actions.Expressions;
using GitHub.Actions.Expressions.Sdk;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens
{
/// <summary>
/// Base class for all template expression tokens
/// </summary>
[DataContract]
public abstract class ExpressionToken : ScalarToken
{
internal ExpressionToken(
Int32 templateType,
Int32? fileId,
Int32? line,
Int32? column,
String directive)
: base(templateType, fileId, line, column)
{
Directive = directive;
}
[DataMember(Name = "directive", EmitDefaultValue = false)]
internal String Directive { get; }
internal static Boolean IsValidExpression(
String expression,
String[] allowedContext,
out Exception ex)
{
// Create dummy named values and functions
var namedValues = new List<INamedValueInfo>();
var functions = new List<IFunctionInfo>();
if (allowedContext?.Length > 0)
{
foreach (var contextItem in allowedContext)
{
var match = s_function.Match(contextItem);
if (match.Success)
{
var functionName = match.Groups[1].Value;
var minParameters = Int32.Parse(match.Groups[2].Value, NumberStyles.None, CultureInfo.InvariantCulture);
var maxParametersRaw = match.Groups[3].Value;
var maxParameters = String.Equals(maxParametersRaw, TemplateConstants.MaxConstant, StringComparison.Ordinal)
? Int32.MaxValue
: Int32.Parse(maxParametersRaw, NumberStyles.None, CultureInfo.InvariantCulture);
functions.Add(new FunctionInfo<DummyFunction>(functionName, minParameters, maxParameters));
}
else
{
namedValues.Add(new NamedValueInfo<ContextValueNode>(contextItem));
}
}
}
// Parse
Boolean result;
ExpressionNode root = null;
try
{
root = new ExpressionParser().CreateTree(expression, null, namedValues, functions) as ExpressionNode;
result = true;
ex = null;
}
catch (Exception exception)
{
result = false;
ex = exception;
}
return result;
}
private sealed class DummyFunction : Function
{
protected override Object EvaluateCore(
EvaluationContext context,
out ResultMemory resultMemory)
{
resultMemory = null;
return null;
}
}
private static readonly Regex s_function = new Regex(@"^([a-zA-Z0-9_]+)\(([0-9]+),([0-9]+|MAX)\)$", RegexOptions.Compiled);
}
}
@@ -0,0 +1,27 @@
using System;
using System.Runtime.Serialization;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens
{
[DataContract]
public sealed class InsertExpressionToken : ExpressionToken
{
public InsertExpressionToken(
Int32? fileId,
Int32? line,
Int32? column)
: base(TokenType.InsertExpression, fileId, line, column, TemplateConstants.InsertDirective)
{
}
public override TemplateToken Clone(Boolean omitSource)
{
return omitSource ? new InsertExpressionToken(null, null, null) : new InsertExpressionToken(FileId, Line, Column);
}
public override String ToString()
{
return $"{TemplateConstants.OpenExpression} insert {TemplateConstants.CloseExpression}";
}
}
}
@@ -0,0 +1,18 @@
using System;
using System.Runtime.Serialization;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens
{
[DataContract]
public abstract class LiteralToken : ScalarToken
{
public LiteralToken(
Int32 tokenType,
Int32? fileId,
Int32? line,
Int32? column)
: base(tokenType, fileId, line, column)
{
}
}
}
@@ -0,0 +1,245 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Runtime.Serialization;
using System.Threading;
using GitHub.Actions.Expressions.Sdk;
using Newtonsoft.Json;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens
{
[DataContract]
[JsonObject]
public sealed class MappingToken : TemplateToken, IEnumerable<KeyValuePair<ScalarToken, TemplateToken>>, IReadOnlyObject
{
public MappingToken(
Int32? fileId,
Int32? line,
Int32? column)
: base(TokenType.Mapping, fileId, line, column)
{
}
internal Int32 Count => m_items?.Count ?? 0;
// IReadOnlyObject (for expressions)
Int32 IReadOnlyObject.Count
{
get
{
InitializeDictionary();
return m_dictionary.Count;
}
}
// IReadOnlyObject (for expressions)
IEnumerable<String> IReadOnlyObject.Keys
{
get
{
InitializeDictionary();
foreach (var key in m_dictionary.Keys)
{
yield return key as String;
}
}
}
// IReadOnlyObject (for expressions)
IEnumerable<Object> IReadOnlyObject.Values
{
get
{
InitializeDictionary();
foreach (var value in m_dictionary.Values)
{
yield return value;
}
}
}
public KeyValuePair<ScalarToken, TemplateToken> this[Int32 index]
{
get
{
return m_items[index];
}
set
{
m_items[index] = value;
m_dictionary = null;
}
}
// IReadOnlyObject (for expressions)
Object IReadOnlyObject.this[String key]
{
get
{
InitializeDictionary();
return m_dictionary[key];
}
}
public void Add(IEnumerable<KeyValuePair<ScalarToken, TemplateToken>> items)
{
foreach (var item in items)
{
Add(item);
}
}
public void Add(KeyValuePair<ScalarToken, TemplateToken> item)
{
if (m_items == null)
{
m_items = new List<KeyValuePair<ScalarToken, TemplateToken>>();
}
m_items.Add(item);
m_dictionary = null;
}
public void Add(
ScalarToken key,
TemplateToken value)
{
Add(new KeyValuePair<ScalarToken, TemplateToken>(key, value));
}
public override TemplateToken Clone(Boolean omitSource)
{
var result = omitSource ? new MappingToken(null, null, null) : new MappingToken(FileId, Line, Column);
if (m_items?.Count > 0)
{
foreach (var pair in m_items)
{
result.Add(pair.Key?.Clone(omitSource) as ScalarToken, pair.Value?.Clone(omitSource));
}
}
return result;
}
public IEnumerator<KeyValuePair<ScalarToken, TemplateToken>> GetEnumerator()
{
if (m_items?.Count > 0)
{
return m_items.GetEnumerator();
}
else
{
return (new List<KeyValuePair<ScalarToken, TemplateToken>>(0)).GetEnumerator();
}
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
if (m_items?.Count > 0)
{
return m_items.GetEnumerator();
}
else
{
return (new KeyValuePair<ScalarToken, TemplateToken>[0]).GetEnumerator();
}
}
public void Insert(
Int32 index,
KeyValuePair<ScalarToken, TemplateToken> item)
{
if (m_items == null)
{
m_items = new List<KeyValuePair<ScalarToken, TemplateToken>>();
}
m_items.Insert(index, item);
m_dictionary = null;
}
public void Insert(
Int32 index,
ScalarToken key,
TemplateToken value)
{
Insert(index, new KeyValuePair<ScalarToken, TemplateToken>(key, value));
}
public void RemoveAt(Int32 index)
{
m_items.RemoveAt(index);
m_dictionary = null;
}
// IReadOnlyObject (for expressions)
Boolean IReadOnlyObject.ContainsKey(String key)
{
InitializeDictionary();
return m_dictionary.Contains(key);
}
// IReadOnlyObject (for expressions)
IEnumerator IReadOnlyObject.GetEnumerator()
{
InitializeDictionary();
return m_dictionary.GetEnumerator();
}
// IReadOnlyObject (for expressions)
Boolean IReadOnlyObject.TryGetValue(
String key,
out Object value)
{
InitializeDictionary();
if (!m_dictionary.Contains(key))
{
value = null;
return false;
}
value = m_dictionary[key];
return true;
}
/// <summary>
/// Initializes the dictionary used for the expressions IReadOnlyObject interface
/// </summary>
private void InitializeDictionary()
{
if (m_dictionary == null)
{
var dictionary = new OrderedDictionary(StringComparer.OrdinalIgnoreCase);
if (m_items?.Count > 0)
{
foreach (var pair in m_items)
{
if (pair.Key is StringToken stringToken &&
!dictionary.Contains(stringToken.Value))
{
dictionary.Add(stringToken.Value, pair.Value);
}
}
}
Interlocked.CompareExchange(ref m_dictionary, dictionary, null);
}
}
[OnSerializing]
private void OnSerializing(StreamingContext context)
{
if (m_items?.Count == 0)
{
m_items = null;
}
}
[DataMember(Name = "map", EmitDefaultValue = false)]
private List<KeyValuePair<ScalarToken, TemplateToken>> m_items;
private IDictionary m_dictionary;
}
}
@@ -0,0 +1,28 @@
using System;
using System.Runtime.Serialization;
using GitHub.Actions.Expressions.Sdk;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens
{
[DataContract]
public sealed class NullToken : LiteralToken, INull
{
public NullToken(
Int32? fileId,
Int32? line,
Int32? column)
: base(TokenType.Null, fileId, line, column)
{
}
public override TemplateToken Clone(Boolean omitSource)
{
return omitSource ? new NullToken(null, null, null) : new NullToken(FileId, Line, Column);
}
public override String ToString()
{
return String.Empty;
}
}
}
@@ -0,0 +1,41 @@
using System;
using System.Globalization;
using System.Runtime.Serialization;
using GitHub.Actions.Expressions.Sdk;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens
{
[DataContract]
public sealed class NumberToken : LiteralToken, INumber
{
public NumberToken(
Int32? fileId,
Int32? line,
Int32? column,
Double value)
: base(TokenType.Number, fileId, line, column)
{
m_value = value;
}
public Double Value => m_value;
public override TemplateToken Clone(Boolean omitSource)
{
return omitSource ? new NumberToken(null, null, null, m_value) : new NumberToken(FileId, Line, Column, m_value);
}
public override String ToString()
{
return m_value.ToString("G15", CultureInfo.InvariantCulture);
}
Double INumber.GetNumber()
{
return Value;
}
[DataMember(Name = "num", EmitDefaultValue = false)]
private Double m_value;
}
}
@@ -0,0 +1,34 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens
{
public abstract class ScalarToken : TemplateToken
{
protected ScalarToken(
Int32 type,
Int32? fileId,
Int32? line,
Int32? column)
: base(type, fileId, line, column)
{
}
public virtual String ToDisplayString()
{
return TrimDisplayString(ToString());
}
protected String TrimDisplayString(String displayString)
{
var firstLine = displayString.TrimStart(' ', '\t', '\r', '\n');
var firstNewLine = firstLine.IndexOfAny(new[] { '\r', '\n' });
if (firstNewLine >= 0)
{
firstLine = firstLine.Substring(0, firstNewLine);
}
return firstLine;
}
}
}
@@ -0,0 +1,149 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization;
using GitHub.Actions.Expressions.Sdk;
using Newtonsoft.Json;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens
{
[DataContract]
[JsonObject]
public sealed class SequenceToken : TemplateToken, IEnumerable<TemplateToken>, IReadOnlyArray
{
public SequenceToken(
Int32? fileId,
Int32? line,
Int32? column)
: base(TokenType.Sequence, fileId, line, column)
{
}
public Int32 Count => m_items?.Count ?? 0;
public TemplateToken this[Int32 index]
{
get
{
return m_items[index];
}
set
{
m_items[index] = value;
}
}
// IReadOnlyArray (for expressions)
Object IReadOnlyArray.this[Int32 index]
{
get
{
return m_items[index];
}
}
public void Add(TemplateToken value)
{
if (m_items == null)
{
m_items = new List<TemplateToken>();
}
m_items.Add(value);
}
public override TemplateToken Clone(Boolean omitSource)
{
var result = omitSource ? new SequenceToken(null, null, null) : new SequenceToken(FileId, Line, Column);
if (m_items?.Count > 0)
{
foreach (var item in m_items)
{
result.Add(item?.Clone(omitSource));
}
}
return result;
}
public IEnumerator<TemplateToken> GetEnumerator()
{
if (m_items?.Count > 0)
{
return m_items.GetEnumerator();
}
else
{
return (new TemplateToken[0] as IEnumerable<TemplateToken>).GetEnumerator();
}
}
IEnumerator IEnumerable.GetEnumerator()
{
if (m_items?.Count > 0)
{
return m_items.GetEnumerator();
}
else
{
return (new TemplateToken[0] as IEnumerable<TemplateToken>).GetEnumerator();
}
}
// IReadOnlyArray (for expressions)
IEnumerator IReadOnlyArray.GetEnumerator()
{
if (m_items?.Count > 0)
{
return m_items.GetEnumerator();
}
else
{
return (new TemplateToken[0] as IEnumerable<TemplateToken>).GetEnumerator();
}
}
public void Insert(
Int32 index,
TemplateToken item)
{
if (m_items == null)
{
m_items = new List<TemplateToken>();
}
m_items.Insert(index, item);
}
public void InsertRange(
Int32 index,
IEnumerable<TemplateToken> items)
{
if (m_items == null)
{
m_items = new List<TemplateToken>();
}
m_items.InsertRange(index, items);
}
public void RemoveAt(Int32 index)
{
m_items.RemoveAt(index);
}
[OnSerializing]
private void OnSerializing(StreamingContext context)
{
if (m_items?.Count == 0)
{
m_items = null;
}
}
[DataMember(Name = "seq", EmitDefaultValue = false)]
private List<TemplateToken> m_items;
}
}
@@ -0,0 +1,62 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using System.Runtime.Serialization;
using GitHub.Actions.Expressions.Sdk;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens
{
[DataContract]
public sealed class StringToken : LiteralToken, IString
{
public StringToken(
Int32? fileId,
Int32? line,
Int32? column,
String value)
: base(TokenType.String, fileId, line, column)
{
m_value = value;
}
public String Value
{
get
{
if (m_value == null)
{
m_value = String.Empty;
}
return m_value;
}
}
public override TemplateToken Clone(Boolean omitSource)
{
return omitSource ? new StringToken(null, null, null, m_value) : new StringToken(FileId, Line, Column, m_value);
}
public override String ToString()
{
return m_value ?? String.Empty;
}
String IString.GetString()
{
return Value;
}
[OnSerializing]
private void OnSerializing(StreamingContext context)
{
if (m_value?.Length == 0)
{
m_value = null;
}
}
[DataMember(Name = "lit", EmitDefaultValue = false)]
private String m_value;
}
}
@@ -0,0 +1,292 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using GitHub.Actions.Expressions;
using GitHub.Actions.Expressions.Sdk;
using Newtonsoft.Json;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens
{
/// <summary>
/// Base class for all template tokens
/// </summary>
[DataContract]
[JsonConverter(typeof(TemplateTokenJsonConverter))]
public abstract class TemplateToken
{
protected TemplateToken(
Int32 type,
Int32? fileId,
Int32? line,
Int32? column)
{
Type = type;
FileId = fileId;
Line = line;
Column = column;
}
[DataMember(Name = "file", EmitDefaultValue = false)]
internal Int32? FileId { get; private set; }
[DataMember(Name = "line", EmitDefaultValue = false)]
internal Int32? Line { get; private set; }
[DataMember(Name = "col", EmitDefaultValue = false)]
internal Int32? Column { get; private set; }
[DataMember(Name = "type", EmitDefaultValue = false)]
internal Int32 Type { get; }
public TemplateToken Clone()
{
return Clone(false);
}
public abstract TemplateToken Clone(Boolean omitSource);
protected StringToken EvaluateStringToken(
TemplateContext context,
String expression,
out Int32 bytes)
{
var originalBytes = context.Memory.CurrentBytes;
try
{
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions);
var options = new EvaluationOptions
{
MaxMemory = context.Memory.MaxBytes,
StrictJsonParsing = context.StrictJsonParsing,
};
var result = tree.Evaluate(context.TraceWriter.ToExpressionTraceWriter(), null, context, options);
if (result.Raw is LiteralToken literalToken)
{
var stringToken = new StringToken(FileId, Line, Column, literalToken.ToString());
context.Memory.AddBytes(stringToken);
return stringToken;
}
if (!result.IsPrimitive)
{
context.Error(this, "Expected a string");
return CreateStringToken(context, expression);
}
var stringValue = result.Kind == ValueKind.Null ? String.Empty : result.ConvertToString();
return CreateStringToken(context, stringValue);
}
finally
{
bytes = context.Memory.CurrentBytes - originalBytes;
}
}
protected SequenceToken EvaluateSequenceToken(
TemplateContext context,
String expression,
out Int32 bytes)
{
var originalBytes = context.Memory.CurrentBytes;
try
{
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions);
var options = new EvaluationOptions
{
MaxMemory = context.Memory.MaxBytes,
StrictJsonParsing = context.StrictJsonParsing,
};
var result = tree.Evaluate(context.TraceWriter.ToExpressionTraceWriter(), null, context, options);
var templateToken = ConvertToTemplateToken(context, result);
if (templateToken is SequenceToken sequence)
{
return sequence;
}
context.Error(this, TemplateStrings.ExpectedSequence());
return CreateSequenceToken(context);
}
finally
{
bytes = context.Memory.CurrentBytes - originalBytes;
}
}
protected MappingToken EvaluateMappingToken(
TemplateContext context,
String expression,
out Int32 bytes)
{
var originalBytes = context.Memory.CurrentBytes;
try
{
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions);
var options = new EvaluationOptions
{
MaxMemory = context.Memory.MaxBytes,
StrictJsonParsing = context.StrictJsonParsing,
};
var result = tree.Evaluate(context.TraceWriter.ToExpressionTraceWriter(), null, context, options);
var templateToken = ConvertToTemplateToken(context, result);
if (templateToken is MappingToken mapping)
{
return mapping;
}
context.Error(this, TemplateStrings.ExpectedMapping());
return CreateMappingToken(context);
}
finally
{
bytes = context.Memory.CurrentBytes - originalBytes;
}
}
protected TemplateToken EvaluateTemplateToken(
TemplateContext context,
String expression,
out Int32 bytes)
{
var originalBytes = context.Memory.CurrentBytes;
try
{
var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions);
var options = new EvaluationOptions
{
MaxMemory = context.Memory.MaxBytes,
StrictJsonParsing = context.StrictJsonParsing,
};
var result = tree.Evaluate(context.TraceWriter.ToExpressionTraceWriter(), null, context, options);
return ConvertToTemplateToken(context, result);
}
finally
{
bytes = context.Memory.CurrentBytes - originalBytes;
}
}
private TemplateToken ConvertToTemplateToken(
TemplateContext context,
EvaluationResult result)
{
// Literal
if (TryConvertToLiteralToken(context, result, out LiteralToken literal))
{
return literal;
}
// Known raw types
else if (!Object.ReferenceEquals(result.Raw, null))
{
if (result.Raw is SequenceToken sequence)
{
context.Memory.AddBytes(sequence, true);
return sequence;
}
else if (result.Raw is MappingToken mapping)
{
context.Memory.AddBytes(mapping, true);
return mapping;
}
}
// Leverage the expression SDK to traverse the object
if (result.TryGetCollectionInterface(out Object collection))
{
if (collection is IReadOnlyObject dictionary)
{
var mapping = CreateMappingToken(context);
foreach (KeyValuePair<String, Object> pair in dictionary)
{
var keyToken = CreateStringToken(context, pair.Key);
var valueResult = EvaluationResult.CreateIntermediateResult(null, pair.Value);
var valueToken = ConvertToTemplateToken(context, valueResult);
mapping.Add(keyToken, valueToken);
}
return mapping;
}
else if (collection is IReadOnlyArray list)
{
var sequence = CreateSequenceToken(context);
foreach (var item in list)
{
var itemResult = EvaluationResult.CreateIntermediateResult(null, item);
var itemToken = ConvertToTemplateToken(context, itemResult);
sequence.Add(itemToken);
}
return sequence;
}
}
throw new ArgumentException(TemplateStrings.UnableToConvertToTemplateToken(result.Value?.GetType().FullName));
}
private Boolean TryConvertToLiteralToken(
TemplateContext context,
EvaluationResult result,
out LiteralToken literal)
{
if (result.Raw is LiteralToken literal2)
{
context.Memory.AddBytes(literal2);
literal = literal2;
return true;
}
switch (result.Kind)
{
case ValueKind.Null:
literal = new NullToken(FileId, Line, Column);
break;
case ValueKind.Boolean:
literal = new BooleanToken(FileId, Line, Column, (Boolean)result.Value);
break;
case ValueKind.Number:
literal = new NumberToken(FileId, Line, Column, (Double)result.Value);
break;
case ValueKind.String:
literal = new StringToken(FileId, Line, Column, (String)result.Value);
break;
default:
literal = null;
return false;
}
context.Memory.AddBytes(literal);
return true;
}
private StringToken CreateStringToken(
TemplateContext context,
String value)
{
var result = new StringToken(FileId, Line, Column, value);
context.Memory.AddBytes(result);
return result;
}
private SequenceToken CreateSequenceToken(TemplateContext context)
{
var result = new SequenceToken(FileId, Line, Column);
context.Memory.AddBytes(result);
return result;
}
private MappingToken CreateMappingToken(TemplateContext context)
{
var result = new MappingToken(FileId, Line, Column);
context.Memory.AddBytes(result);
return result;
}
}
}
@@ -0,0 +1,291 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using System.Collections.Generic;
using System.Linq;
using GitHub.Actions.Expressions;
using GitHub.Actions.Expressions.Sdk;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens
{
internal static class TemplateTokenExtensions
{
internal static BooleanToken AssertBoolean(
this TemplateToken value,
string objectDescription)
{
if (value is BooleanToken booleanToken)
{
return booleanToken;
}
throw new ArgumentException($"Unexpected type '{value?.GetType().Name}' encountered while reading '{objectDescription}'. The type '{nameof(BooleanToken)}' was expected.");
}
internal static NullToken AssertNull(
this TemplateToken value,
string objectDescription)
{
if (value is NullToken nullToken)
{
return nullToken;
}
throw new ArgumentException($"Unexpected type '{value?.GetType().Name}' encountered while reading '{objectDescription}'. The type '{nameof(NullToken)}' was expected.");
}
internal static NumberToken AssertNumber(
this TemplateToken value,
string objectDescription)
{
if (value is NumberToken numberToken)
{
return numberToken;
}
throw new ArgumentException($"Unexpected type '{value?.GetType().Name}' encountered while reading '{objectDescription}'. The type '{nameof(NumberToken)}' was expected.");
}
internal static StringToken AssertString(
this TemplateToken value,
string objectDescription)
{
if (value is StringToken stringToken)
{
return stringToken;
}
throw new ArgumentException($"Unexpected type '{value?.GetType().Name}' encountered while reading '{objectDescription}'. The type '{nameof(StringToken)}' was expected.");
}
internal static MappingToken AssertMapping(
this TemplateToken value,
string objectDescription)
{
if (value is MappingToken mapping)
{
return mapping;
}
throw new ArgumentException($"Unexpected type '{value?.GetType().Name}' encountered while reading '{objectDescription}'. The type '{nameof(MappingToken)}' was expected.");
}
internal static ScalarToken AssertScalar(
this TemplateToken value,
string objectDescription)
{
if (value is ScalarToken scalar)
{
return scalar;
}
throw new ArgumentException($"Unexpected type '{value?.GetType().Name}' encountered while reading '{objectDescription}'. The type '{nameof(ScalarToken)}' was expected.");
}
internal static SequenceToken AssertSequence(
this TemplateToken value,
string objectDescription)
{
if (value is SequenceToken sequence)
{
return sequence;
}
throw new ArgumentException($"Unexpected type '{value?.GetType().Name}' encountered while reading '{objectDescription}'. The type '{nameof(SequenceToken)}' was expected.");
}
internal static void AssertUnexpectedValue(
this LiteralToken literal,
string objectDescription)
{
throw new ArgumentException($"Error while reading '{objectDescription}'. Unexpected value '{literal.ToString()}'");
}
/// <summary>
/// Traverses the token and checks whether all required expression values
/// and functions are provided.
/// </summary>
public static bool CheckHasRequiredContext(
this TemplateToken token,
IReadOnlyObject expressionValues,
IList<IFunctionInfo> expressionFunctions)
{
var expressionTokens = token.Traverse()
.OfType<BasicExpressionToken>()
.ToArray();
var parser = new ExpressionParser();
foreach (var expressionToken in expressionTokens)
{
var tree = parser.ValidateSyntax(expressionToken.Expression, null);
foreach (var node in tree.Traverse())
{
if (node is NamedValue namedValue)
{
if (expressionValues?.Keys.Any(x => string.Equals(x, namedValue.Name, StringComparison.OrdinalIgnoreCase)) != true)
{
return false;
}
}
else if (node is Function function &&
!ExpressionConstants.WellKnownFunctions.ContainsKey(function.Name) &&
expressionFunctions?.Any(x => string.Equals(x.Name, function.Name, StringComparison.OrdinalIgnoreCase)) != true)
{
return false;
}
}
}
return true;
}
/// <summary>
/// Traverses each token that is provided (including descendants) and
/// checks whether specific contexts or sub-properties of contexts are referenced.
/// If a conclusive determination cannot be made, then the pattern is considered matched.
/// For example, the expression "toJson(github)" matches the pattern "github.event" because
/// the value is passed to a function. Not enough information is known to determine whether
/// the function requires the sub-property. Therefore, it is assumed that it may.
///
/// Wildcards are supported in the pattern, and are treated as matching any literal.
/// For example, the expression "needs.my-job.outputs.my-output" matches the pattern "needs.*.outputs".
/// </summary>
public static bool[] CheckReferencesContext(
this IList<TemplateToken> tokens,
params string[] patterns)
{
var result = new bool[patterns.Length];
var expressionTokens = tokens
.SelectMany(x => x.Traverse())
.OfType<BasicExpressionToken>()
.ToArray();
var parser = new ExpressionParser();
foreach (var expressionToken in expressionTokens)
{
var tree = parser.ValidateSyntax(expressionToken.Expression, null);
var isReferenced = tree.CheckReferencesContext(patterns);
for (var i = 0; i < patterns.Length; i++)
{
if (isReferenced[i])
{
result[i] = true;
}
}
}
return result;
}
/// <summary>
/// Returns all tokens (depth first)
/// </summary>
public static IEnumerable<TemplateToken> Traverse(this TemplateToken token)
{
return Traverse(token, omitKeys: false);
}
/// <summary>
/// Returns all tokens (depth first)
/// </summary>
public static IEnumerable<TemplateToken> Traverse(
this TemplateToken token,
bool omitKeys)
{
if (token != null)
{
yield return token;
if (token is SequenceToken || token is MappingToken)
{
var state = new TraversalState(null, token);
while (state != null)
{
if (state.MoveNext(omitKeys))
{
token = state.Current;
yield return token;
if (token is SequenceToken || token is MappingToken)
{
state = new TraversalState(state, token);
}
}
else
{
state = state.Parent;
}
}
}
}
}
private sealed class TraversalState
{
public TraversalState(
TraversalState parent,
TemplateToken token)
{
Parent = parent;
m_token = token;
}
public bool MoveNext(bool omitKeys)
{
switch (m_token.Type)
{
case TokenType.Sequence:
var sequence = m_token as SequenceToken;
if (++m_index < sequence.Count)
{
Current = sequence[m_index];
return true;
}
else
{
Current = null;
return false;
}
case TokenType.Mapping:
var mapping = m_token as MappingToken;
// Return the value
if (m_isKey)
{
m_isKey = false;
Current = mapping[m_index].Value;
return true;
}
if (++m_index < mapping.Count)
{
// Skip the key, return the value
if (omitKeys)
{
m_isKey = false;
Current = mapping[m_index].Value;
return true;
}
// Return the key
m_isKey = true;
Current = mapping[m_index].Key;
return true;
}
Current = null;
return false;
default:
throw new NotSupportedException($"Unexpected token type '{m_token.Type}'");
}
}
private TemplateToken m_token;
private int m_index = -1;
private bool m_isKey;
public TemplateToken Current;
public TraversalState Parent;
}
}
}
@@ -0,0 +1,370 @@
#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references
using System;
using System.Reflection;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens
{
/// <summary>
/// JSON serializer for TemplateToken objects
/// </summary>
internal sealed class TemplateTokenJsonConverter : JsonConverter
{
public override Boolean CanWrite
{
get
{
return true;
}
}
public override Boolean CanConvert(Type objectType)
{
return typeof(TemplateToken).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
}
public override Object ReadJson(
JsonReader reader,
Type objectType,
Object existingValue,
JsonSerializer serializer)
{
switch (reader.TokenType)
{
case JsonToken.String:
return new StringToken(null, null, null, reader.Value.ToString());
case JsonToken.Boolean:
return new BooleanToken(null, null, null, (Boolean)reader.Value);
case JsonToken.Float:
return new NumberToken(null, null, null, (Double)reader.Value);
case JsonToken.Integer:
return new NumberToken(null, null, null, (Double)(Int64)reader.Value);
case JsonToken.Null:
return new NullToken(null, null, null);
case JsonToken.StartObject:
break;
default:
return null;
}
Int32? type = null;
JObject value = JObject.Load(reader);
if (!value.TryGetValue("type", StringComparison.OrdinalIgnoreCase, out JToken typeValue))
{
type = TokenType.String;
}
else if (typeValue.Type == JTokenType.Integer)
{
type = (Int32)typeValue;
}
else
{
return existingValue;
}
Object newValue = null;
switch (type)
{
case TokenType.Null:
newValue = new NullToken(null, null, null);
break;
case TokenType.Boolean:
newValue = new BooleanToken(null, null, null, default(Boolean));
break;
case TokenType.Number:
newValue = new NumberToken(null, null, null, default(Double));
break;
case TokenType.String:
newValue = new StringToken(null, null, null, null);
break;
case TokenType.BasicExpression:
newValue = new BasicExpressionToken(null, null, null, null);
break;
case TokenType.InsertExpression:
newValue = new InsertExpressionToken(null, null, null);
break;
case TokenType.Sequence:
newValue = new SequenceToken(null, null, null);
break;
case TokenType.Mapping:
newValue = new MappingToken(null, null, null);
break;
}
if (value != null)
{
using JsonReader objectReader = value.CreateReader();
serializer.Populate(objectReader, newValue);
}
return newValue;
}
public override void WriteJson(
JsonWriter writer,
Object value,
JsonSerializer serializer)
{
if (value is TemplateToken token)
{
switch (token.Type)
{
case TokenType.Null:
if (token.FileId == null && token.Line == null && token.Column == null)
{
writer.WriteNull();
}
else
{
writer.WriteStartObject();
writer.WritePropertyName("type");
writer.WriteValue(token.Type);
if (token.FileId != null)
{
writer.WritePropertyName("file");
writer.WriteValue(token.FileId);
}
if (token.Line != null)
{
writer.WritePropertyName("line");
writer.WriteValue(token.Line);
}
if (token.Column != null)
{
writer.WritePropertyName("col");
writer.WriteValue(token.Column);
}
writer.WriteEndObject();
}
return;
case TokenType.Boolean:
var booleanToken = token as BooleanToken;
if (token.FileId == null && token.Line == null && token.Column == null)
{
writer.WriteValue(booleanToken.Value);
}
else
{
writer.WriteStartObject();
writer.WritePropertyName("type");
writer.WriteValue(token.Type);
if (token.FileId != null)
{
writer.WritePropertyName("file");
writer.WriteValue(token.FileId);
}
if (token.Line != null)
{
writer.WritePropertyName("line");
writer.WriteValue(token.Line);
}
if (token.Column != null)
{
writer.WritePropertyName("col");
writer.WriteValue(token.Column);
}
writer.WritePropertyName("bool");
writer.WriteValue(booleanToken.Value);
writer.WriteEndObject();
}
return;
case TokenType.Number:
var numberToken = token as NumberToken;
if (token.FileId == null && token.Line == null && token.Column == null)
{
writer.WriteValue(numberToken.Value);
}
else
{
writer.WriteStartObject();
writer.WritePropertyName("type");
writer.WriteValue(token.Type);
if (token.FileId != null)
{
writer.WritePropertyName("file");
writer.WriteValue(token.FileId);
}
if (token.Line != null)
{
writer.WritePropertyName("line");
writer.WriteValue(token.Line);
}
if (token.Column != null)
{
writer.WritePropertyName("col");
writer.WriteValue(token.Column);
}
writer.WritePropertyName("num");
writer.WriteValue(numberToken.Value);
writer.WriteEndObject();
}
return;
case TokenType.String:
var stringToken = token as StringToken;
if (token.FileId == null && token.Line == null && token.Column == null)
{
writer.WriteValue(stringToken.Value);
}
else
{
writer.WriteStartObject();
writer.WritePropertyName("type");
writer.WriteValue(token.Type);
if (token.FileId != null)
{
writer.WritePropertyName("file");
writer.WriteValue(token.FileId);
}
if (token.Line != null)
{
writer.WritePropertyName("line");
writer.WriteValue(token.Line);
}
if (token.Column != null)
{
writer.WritePropertyName("col");
writer.WriteValue(token.Column);
}
writer.WritePropertyName("lit");
writer.WriteValue(stringToken.Value);
writer.WriteEndObject();
}
return;
case TokenType.BasicExpression:
var basicExpressionToken = token as BasicExpressionToken;
writer.WriteStartObject();
writer.WritePropertyName("type");
writer.WriteValue(token.Type);
if (token.FileId != null)
{
writer.WritePropertyName("file");
writer.WriteValue(token.FileId);
}
if (token.Line != null)
{
writer.WritePropertyName("line");
writer.WriteValue(token.Line);
}
if (token.Column != null)
{
writer.WritePropertyName("col");
writer.WriteValue(token.Column);
}
if (!String.IsNullOrEmpty(basicExpressionToken.Expression))
{
writer.WritePropertyName("expr");
writer.WriteValue(basicExpressionToken.Expression);
}
writer.WriteEndObject();
return;
case TokenType.InsertExpression:
var insertExpressionToken = token as InsertExpressionToken;
writer.WriteStartObject();
writer.WritePropertyName("type");
writer.WriteValue(token.Type);
if (token.FileId != null)
{
writer.WritePropertyName("file");
writer.WriteValue(token.FileId);
}
if (token.Line != null)
{
writer.WritePropertyName("line");
writer.WriteValue(token.Line);
}
if (token.Column != null)
{
writer.WritePropertyName("col");
writer.WriteValue(token.Column);
}
writer.WritePropertyName("directive");
writer.WriteValue(insertExpressionToken.Directive);
writer.WriteEndObject();
return;
case TokenType.Sequence:
var sequenceToken = token as SequenceToken;
writer.WriteStartObject();
writer.WritePropertyName("type");
writer.WriteValue(token.Type);
if (token.FileId != null)
{
writer.WritePropertyName("file");
writer.WriteValue(token.FileId);
}
if (token.Line != null)
{
writer.WritePropertyName("line");
writer.WriteValue(token.Line);
}
if (token.Column != null)
{
writer.WritePropertyName("col");
writer.WriteValue(token.Column);
}
if (sequenceToken.Count > 0)
{
writer.WritePropertyName("seq");
writer.WriteStartArray();
foreach (var item in sequenceToken)
{
serializer.Serialize(writer, item);
}
writer.WriteEndArray();
}
writer.WriteEndObject();
return;
case TokenType.Mapping:
var mappingToken = token as MappingToken;
writer.WriteStartObject();
writer.WritePropertyName("type");
writer.WriteValue(token.Type);
if (token.FileId != null)
{
writer.WritePropertyName("file");
writer.WriteValue(token.FileId);
}
if (token.Line != null)
{
writer.WritePropertyName("line");
writer.WriteValue(token.Line);
}
if (token.Column != null)
{
writer.WritePropertyName("col");
writer.WriteValue(token.Column);
}
if (mappingToken.Count > 0)
{
writer.WritePropertyName("map");
writer.WriteStartArray();
foreach (var item in mappingToken)
{
serializer.Serialize(writer, item);
}
writer.WriteEndArray();
}
writer.WriteEndObject();
return;
}
}
throw new NotSupportedException($"Unexpected type '{value?.GetType().FullName}' when serializing template token");
}
}
}
@@ -0,0 +1,23 @@
using System;
namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens
{
internal static class TokenType
{
internal const Int32 String = 0;
internal const Int32 Sequence = 1;
internal const Int32 Mapping = 2;
internal const Int32 BasicExpression = 3;
internal const Int32 InsertExpression = 4;
internal const Int32 Boolean = 5;
internal const Int32 Number = 6;
internal const Int32 Null = 7;
}
}