From d5495443b94cd0d201517226157c82db20706d6d Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Thu, 27 Jun 2024 15:36:04 +0200 Subject: [PATCH] Qute: fix possible stack overflow error in InsertSectionHelper - fixes #41451 (cherry picked from commit 8230825f69ee2ce3b38fb7155e5ab262665a48e5) --- .../io/quarkus/qute/InsertSectionHelper.java | 32 ++++++- .../io/quarkus/qute/ResolutionContext.java | 13 ++- .../quarkus/qute/ResolutionContextImpl.java | 13 +++ .../java/io/quarkus/qute/SectionNode.java | 5 + .../java/io/quarkus/qute/TemplateNode.java | 4 + .../java/io/quarkus/qute/IncludeTest.java | 96 +++++++++++++++++++ 6 files changed, 158 insertions(+), 5 deletions(-) diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/InsertSectionHelper.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/InsertSectionHelper.java index b018033f111ea..77739382ae6c0 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/InsertSectionHelper.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/InsertSectionHelper.java @@ -15,12 +15,38 @@ public InsertSectionHelper(String name, SectionBlock defaultBlock) { @Override public CompletionStage resolve(SectionResolutionContext context) { - SectionBlock extending = context.resolutionContext().getExtendingBlock(name); + // Note that {#insert} is evaluated on the current resolution context + // Therefore, we need to try to find the "correct" parent context to avoid stack + // overflow errors when using the same block names + ResolutionContext rc = findParentResolutionContext(context.resolutionContext()); + if (rc == null) { + // No parent context found - use the current + rc = context.resolutionContext(); + } + SectionBlock extending = rc.getExtendingBlock(name); if (extending != null) { - return context.execute(extending, context.resolutionContext()); + return context.execute(extending, rc); } else { - return context.execute(defaultBlock, context.resolutionContext()); + return context.execute(defaultBlock, rc); + } + } + + private ResolutionContext findParentResolutionContext(ResolutionContext context) { + if (context.getParent() == null) { + return null; } + // Let's iterate over all extending blocks and try to find the "correct" parent context + // The "correct" parent context is the parent of a context that contains this helper + // instance in any of its extending block + SectionBlock block = context.getCurrentExtendingBlock(name); + if (block != null && block.findNode(this::containsThisHelperInstance) != null) { + return context.getParent(); + } + return findParentResolutionContext(context.getParent()); + } + + private boolean containsThisHelperInstance(TemplateNode node) { + return node.isSection() && node.asSection().helper == this; } public static class Factory implements SectionHelperFactory { diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContext.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContext.java index ea293a2ca9ad8..7fd6d0a2d5ed3 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContext.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContext.java @@ -4,7 +4,7 @@ import java.util.concurrent.CompletionStage; /** - * + * The resolution context holds the current context object. */ public interface ResolutionContext { @@ -46,12 +46,21 @@ public interface ResolutionContext { ResolutionContext getParent(); /** + * If no extending block exists for the given name then the parent context (if present) is queried. * * @param name - * @return the extending block for the specified name or null + * @return the extending block for the specified name or {@code null} */ SectionBlock getExtendingBlock(String name); + /** + * Unlike {@link #getExtendingBlock(String)} this method never queries the parent context. + * + * @param name + * @return the extending block for the specified name or {@code null} + */ + SectionBlock getCurrentExtendingBlock(String name); + /** * * @param key diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContextImpl.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContextImpl.java index b9eb6e794ecaf..d8714b8cda827 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContextImpl.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/ResolutionContextImpl.java @@ -52,6 +52,11 @@ public SectionBlock getExtendingBlock(String name) { return null; } + @Override + public SectionBlock getCurrentExtendingBlock(String name) { + return getExtendingBlock(name); + } + @Override public Object getAttribute(String key) { return attributeFun.apply(key); @@ -116,6 +121,14 @@ public SectionBlock getExtendingBlock(String name) { return null; } + @Override + public SectionBlock getCurrentExtendingBlock(String name) { + if (extendingBlocks != null) { + return extendingBlocks.get(name); + } + return null; + } + @Override public Object getAttribute(String key) { return parent.getAttribute(key); diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionNode.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionNode.java index e90f6cc4d3d5f..6231b2527aa8c 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionNode.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/SectionNode.java @@ -67,6 +67,11 @@ public boolean isSection() { return true; } + @Override + public SectionNode asSection() { + return this; + } + void optimizeNodes(Set nodes) { for (SectionBlock block : blocks) { block.optimizeNodes(nodes); diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateNode.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateNode.java index 23096cb61698c..034c6e498d982 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateNode.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateNode.java @@ -57,6 +57,10 @@ default boolean isSection() { return false; } + default SectionNode asSection() { + throw new IllegalStateException(); + } + /** * Represents an origin of a template node. */ diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/IncludeTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/IncludeTest.java index 316e046642fa9..0482275186af2 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/IncludeTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/IncludeTest.java @@ -251,4 +251,100 @@ public void testIsolation() { assertEquals("NOT_FOUND", engine.parse("{#include foo _isolated /}").data("name", "Dorka").render()); } + @Test + public void testNestedMainBlocks() { + Engine engine = Engine.builder() + .addDefaults() + .build(); + + engine.putTemplate("root", engine.parse(""" + + {#insert /} + + """)); + engine.putTemplate("auth", engine.parse(""" + {#include root} +
+ {#insert /} +
+ {/include} + """)); + assertEquals("
LoginForm
" + + "", engine.parse(""" + {#include auth} +
Login Form
+ {/include} + """).render().replaceAll("\\s+", "")); + + engine.putTemplate("next", engine.parse(""" + {#include auth} + + {#insert /} + + {/include} + """)); + + // 1. top -> push child rc#1 with extending block $default$ + // 2. next -> push child rc#2 with extending block $default$ + // 3. auth -> push child rc#3 with extending block $default$ + // 4. root -> eval {#insert}, looks up $default$ in rc#3 + // 5. auth -> eval {#insert}, looks up $default$ in rc#2 + // 6. next -> eval {#insert}, looks up $default$ in rc#1 + assertEquals("
LoginForm
" + + "", engine.parse(""" + {#include next} +
Login Form
+ {/include} + """).render().replaceAll("\\s+", "")); + } + + @Test + public void testNestedBlocksWithSameName() { + Engine engine = Engine.builder() + .addDefaults() + .build(); + + engine.putTemplate("root", engine.parse(""" + + {#insert foo /} + + """)); + engine.putTemplate("auth", engine.parse(""" + {#include root} + {#foo} +
+ {#insert foo /} +
+ {/foo} + {/include} + """)); + assertEquals("
LoginForm
" + + "", engine.parse(""" + {#include auth} + {#foo} +
Login Form
+ {/foo} + {/include} + """).render().replaceAll("\\s+", "")); + + engine.putTemplate("next", engine.parse(""" + {#include auth} + {#foo} + + {#insert foo /} + + {/foo} + {/include} + """)); + + assertEquals("
LoginForm
" + + "", engine.parse(""" + {#include next} + {#foo} +
Login Form
+ {/foo} + {/include} + """).render().replaceAll("\\s+", "")); + } + }