Skip to content

Commit

Permalink
Merge branch 'master' into license-expression
Browse files Browse the repository at this point in the history
  • Loading branch information
oformaniuk authored Apr 1, 2024
2 parents 194b3b8 + 80727a7 commit 0c6a1ad
Show file tree
Hide file tree
Showing 15 changed files with 228 additions and 34 deletions.
13 changes: 0 additions & 13 deletions .github/FUNDING.yml

This file was deleted.

5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ jobs:
dotnet-version: 3.1.x
- uses: actions/setup-java@v4
with:
java-version: '17' # The JDK version to make available on the path.
java-version: '21' # The JDK version to make available on the path.
distribution: 'zulu'
- name: Clean package cache as a temporary workaround for https://github.com/actions/setup-dotnet/issues/155
working-directory: ./source
run: dotnet clean -c Release && dotnet nuget locals all --clear
Expand Down Expand Up @@ -144,4 +145,4 @@ jobs:
steps:
- uses: release-drafter/release-drafter@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
7 changes: 4 additions & 3 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,10 @@ jobs:
uses: actions/setup-dotnet@v1
with:
dotnet-version: 3.1.x
- uses: actions/setup-java@v1
- uses: actions/setup-java@v4
with:
java-version: '17' # The JDK version to make available on the path.
java-version: '21' # The JDK version to make available on the path.
distribution: 'zulu'
- name: Clean package cache as a temporary workaround for https://github.com/actions/setup-dotnet/issues/155
working-directory: ./source
run: dotnet clean -c Release && dotnet nuget locals all --clear
Expand Down Expand Up @@ -136,4 +137,4 @@ jobs:
uses: actions/upload-artifact@v2
with:
name: Benchmark
path: source/Handlebars.Benchmark/BenchmarkDotNet.Artifacts/results/
path: source/Handlebars.Benchmark/BenchmarkDotNet.Artifacts/results/
9 changes: 9 additions & 0 deletions source/Handlebars.Test/ExceptionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,14 @@ public void TestLooseClosingBlockInIteratorExpressionException()
Handlebars.Compile("{{#each enumerateMe}}test{{/if}}{{/each}}")(data);
});
}

[Fact]
public void TestNonClosingIgnoreBlockException()
{
Assert.Throws<HandlebarsParserException>(() =>
{
Handlebars.Compile("{{ [test }}")(new { });
});
}
}
}
60 changes: 59 additions & 1 deletion source/Handlebars.Test/InlinePartialTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Xunit;
using System.Collections.Generic;
using Xunit;

namespace HandlebarsDotNet.Test
{
Expand Down Expand Up @@ -355,6 +356,63 @@ public void InlinePartialInEach()
var result = template(data);
Assert.Equal("12", result);
}

[Fact]
public void RecursionUnboundedInlinePartial()
{
string source = "{{#*inline \"list\"}}{{>list}}{{/inline}}{{>list}}";

var template = Handlebars.Compile(source);

string Result() => template(null);
var ex = Assert.Throws<HandlebarsRuntimeException>(Result);
while (ex.InnerException != null)
ex = Assert.IsType<HandlebarsRuntimeException>(ex.InnerException);
Assert.Equal("Runtime error while rendering partial 'list', exceeded recursion depth limit of 100", ex.Message);
}

[Fact]
public void RecursionBoundedToLimitInlinePartial()
{
string source = "{{#*inline \"list\"}}x{{#each items}}{{#if items}}{{>list}}{{/if}}{{/each}}{{/inline}}{{>list}}{{>list}}";

var template = Handlebars.Compile(source);

var data = new Dictionary<string, object>();
var items = data;
for (var depth = 0; depth < Handlebars.Configuration.PartialRecursionDepthLimit; depth++)
{
var nestedItems = new Dictionary<string, object>();
items.Add("items", new[] { nestedItems });
items = nestedItems;
}

var result = template(data);
Assert.Equal(new string('x', Handlebars.Configuration.PartialRecursionDepthLimit * 2), result);
}

[Fact]
public void RecursionBoundedAboveLimitInlinePartial()
{
string source = "{{#*inline \"list\"}}x{{#each items}}{{#if items}}{{>list}}{{/if}}{{/each}}{{/inline}}{{>list}}";

var template = Handlebars.Compile(source);

var data = new Dictionary<string, object>();
var items = data;
for (var depth = 0; depth < Handlebars.Configuration.PartialRecursionDepthLimit + 1; depth++)
{
var nestedItems = new Dictionary<string, object>();
items.Add("items", new[] { nestedItems });
items = nestedItems;
}

string Result() => template(data);
var ex = Assert.Throws<HandlebarsRuntimeException>(Result);
while (ex.InnerException != null)
ex = Assert.IsType<HandlebarsRuntimeException>(ex.InnerException);
Assert.Equal("Runtime error while rendering partial 'list', exceeded recursion depth limit of 100", ex.Message);
}
}
}

102 changes: 102 additions & 0 deletions source/Handlebars.Test/PartialTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,30 @@ public void BlockPartialWithNestedSpecialNamedPartial2()
Assert.Equal("A 1 B 3 C 4 D 2 E", result);
}

[Fact]
public void RecursionUnboundedBlockPartialWithSpecialNamedPartial()
{
string source = "{{#>myPartial}}{{>myPartial}}{{/myPartial}}";

var template = Handlebars.Compile(source);

var partialSource = "{{> @partial-block }}";
using (var reader = new StringReader(partialSource))
{
var partialTemplate = Handlebars.Compile(reader);
Handlebars.RegisterTemplate("myPartial", partialTemplate);
}

string Result() => template(null);
var ex = Assert.Throws<HandlebarsRuntimeException>(Result);
Assert.Equal("Runtime error while rendering partial 'myPartial', see inner exception for more information", ex.Message);
ex = Assert.IsType<HandlebarsRuntimeException>(ex.InnerException);
Assert.Equal("Runtime error while rendering partial 'myPartial', see inner exception for more information", ex.Message);
ex = Assert.IsType<HandlebarsRuntimeException>(ex.InnerException);
Assert.Equal("Referenced partial name @partial-block could not be resolved", ex.Message);
Assert.Null(ex.InnerException);
}

[Fact]
public void TemplateWithSpecialNamedPartial()
{
Expand Down Expand Up @@ -717,6 +741,84 @@ public void SubExpressionPartial()
var result = template(data);
Assert.Equal("Hello, world!", result);
}

[Fact]
public void RecursionUnboundedPartial()
{
string source = "{{>list}}";

var template = Handlebars.Compile(source);

var partialSource = "{{>list}}";
using (var reader = new StringReader(partialSource))
{
var partialTemplate = Handlebars.Compile(reader);
Handlebars.RegisterTemplate("list", partialTemplate);
}

string Result() => template(null);
var ex = Assert.Throws<HandlebarsRuntimeException>(Result);
while (ex.InnerException != null)
ex = Assert.IsType<HandlebarsRuntimeException>(ex.InnerException);
Assert.Equal("Runtime error while rendering partial 'list', exceeded recursion depth limit of 100", ex.Message);
}

[Fact]
public void RecursionBoundedToLimitPartial()
{
string source = "{{>list}}{{>list}}";

var template = Handlebars.Compile(source);

var partialSource = "x{{#each items}}{{#if items}}{{>list}}{{/if}}{{/each}}";
using (var reader = new StringReader(partialSource))
{
var partialTemplate = Handlebars.Compile(reader);
Handlebars.RegisterTemplate("list", partialTemplate);
}

var data = new Dictionary<string, object>();
var items = data;
for (var depth = 0; depth < Handlebars.Configuration.PartialRecursionDepthLimit; depth++)
{
var nestedItems = new Dictionary<string, object>();
items.Add("items", new[] { nestedItems });
items = nestedItems;
}

var result = template(data);
Assert.Equal(new string('x', Handlebars.Configuration.PartialRecursionDepthLimit * 2), result);
}

[Fact]
public void RecursionBoundedAboveLimitPartial()
{
string source = "{{>list}}";

var template = Handlebars.Compile(source);

var partialSource = "x{{#each items}}{{#if items}}{{>list}}{{/if}}{{/each}}";
using (var reader = new StringReader(partialSource))
{
var partialTemplate = Handlebars.Compile(reader);
Handlebars.RegisterTemplate("list", partialTemplate);
}

var data = new Dictionary<string, object>();
var items = data;
for (var depth = 0; depth < Handlebars.Configuration.PartialRecursionDepthLimit + 1; depth++)
{
var nestedItems = new Dictionary<string, object>();
items.Add("items", new[] { nestedItems });
items = nestedItems;
}

string Result() => template(data);
var ex = Assert.Throws<HandlebarsRuntimeException>(Result);
while (ex.InnerException != null)
ex = Assert.IsType<HandlebarsRuntimeException>(ex.InnerException);
Assert.Equal("Runtime error while rendering partial 'list', exceeded recursion depth limit of 100", ex.Message);
}
}
}

5 changes: 4 additions & 1 deletion source/Handlebars.Test/PathInfoTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ public void DotPath()
[InlineData("a/[b.c].[b/c]/d", new [] {"a", "[b.c].[b/c]", "d"})]
[InlineData("a/[b/c]/d", new [] {"a", "[b/c]", "d"})]
[InlineData("a/[b.c/d]/e", new [] {"a", "[b.c/d]", "e"})]
[InlineData("a/[b.c/d/e/f]/e", new [] {"a", "[b.c/d/e/f]", "e"})]
[InlineData("a/[a//b/c.d/e]/e", new [] {"a", "[a//b/c.d/e]", "e"})]
[InlineData("a/[b/c/d].[e/f/g]/h", new [] {"a", "[b/c/d].[e/f/g]", "h"})]
public void SlashPath(string input, string[] expected)
{
var pathInfo = PathInfo.Parse(input);
Expand All @@ -62,4 +65,4 @@ public void SlashPath(string input, string[] expected)
}
}
}
}
}
2 changes: 2 additions & 0 deletions source/Handlebars/BindingContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ out WellKnownVariables[(int) WellKnownVariable.Parent]

internal TemplateDelegate PartialBlockTemplate { get; set; }

internal short PartialDepth { get; set; }

public object Value { get; set; }

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down
20 changes: 10 additions & 10 deletions source/Handlebars/Compiler/Lexer/Parsers/WordParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,7 @@ private static string AccumulateWord(ExtendedStringReader reader)

while (true)
{
if (isEscaped)
{
var c = (char) reader.Read();
if (c == ']') isEscaped = false;

buffer.Append(c);
continue;
}

if (!inString)
if (!inString && !isEscaped)
{
var peek = (char) reader.Peek();

Expand All @@ -70,6 +61,15 @@ private static string AccumulateWord(ExtendedStringReader reader)
throw new HandlebarsParserException("Reached end of template before the expression was closed.", reader.GetContext());
}

if (isEscaped)
{
var c = (char) node;
if (c == ']') isEscaped = false;

buffer.Append(c);
continue;
}

if (node == '[' && !inString)
{
isEscaped = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,24 @@ private static bool InvokePartial(
return true;
}

void IncreaseDepth()
{
if (++context.PartialDepth > configuration.PartialRecursionDepthLimit)
throw new HandlebarsRuntimeException($"Runtime error while rendering partial '{partialName}', exceeded recursion depth limit of {configuration.PartialRecursionDepthLimit}");
}

//if we have an inline partial, skip the file system and RegisteredTemplates collection
if (context.InlinePartialTemplates.TryGetValue(partialName, out var partial))
{
partial(writer, context);
IncreaseDepth();
try
{
partial(writer, context);
}
finally
{
context.PartialDepth--;
}
return true;
}

Expand All @@ -152,6 +166,7 @@ private static bool InvokePartial(
}
}

IncreaseDepth();
try
{
using var textWriter = writer.CreateWrapper();
Expand All @@ -162,6 +177,10 @@ private static bool InvokePartial(
{
throw new HandlebarsRuntimeException($"Runtime error while rendering partial '{partialName}', see inner exception for more information", exception);
}
finally
{
context.PartialDepth--;
}
}
}
}
5 changes: 5 additions & 0 deletions source/Handlebars/Configuration/HandlebarsConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ public string UnresolvedBindingFormatter
/// </summary>
public IMissingPartialTemplateHandler MissingPartialTemplateHandler { get; set; }

/// <summary>
/// Maximum depth to recurse into partial templates when evaluating the template. Defaults to 100.
/// </summary>
public short PartialRecursionDepthLimit { get; set; } = 100;

/// <inheritdoc cref="IMemberAliasProvider"/>
public IAppendOnlyList<IMemberAliasProvider> AliasProviders { get; } = new ObservableList<IMemberAliasProvider>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public HandlebarsConfigurationAdapter(HandlebarsConfiguration configuration)
public bool ThrowOnUnresolvedBindingExpression => UnderlingConfiguration.ThrowOnUnresolvedBindingExpression;
public IPartialTemplateResolver PartialTemplateResolver => UnderlingConfiguration.PartialTemplateResolver;
public IMissingPartialTemplateHandler MissingPartialTemplateHandler => UnderlingConfiguration.MissingPartialTemplateHandler;
public short PartialRecursionDepthLimit => UnderlingConfiguration.PartialRecursionDepthLimit;
public Compatibility Compatibility => UnderlingConfiguration.Compatibility;

public bool NoEscape => UnderlingConfiguration.NoEscape;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public interface ICompiledHandlebarsConfiguration : IHandlebarsTemplateRegistrat

IMissingPartialTemplateHandler MissingPartialTemplateHandler { get; }

short PartialRecursionDepthLimit { get; }

IIndexed<PathInfoLight, Ref<IHelperDescriptor<HelperOptions>>> Helpers { get; }

IIndexed<PathInfoLight, Ref<IHelperDescriptor<BlockHelperOptions>>> BlockHelpers { get; }
Expand Down
Loading

0 comments on commit 0c6a1ad

Please sign in to comment.