diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index 82b8259f7aa01..6f49e38ac860f 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -1525,6 +1525,8 @@ If you want to use Qute in your Quarkus application, add the following dependenc In Quarkus, a preconfigured engine instance is provided and available for injection - a bean with scope `@ApplicationScoped`, bean type `io.quarkus.qute.Engine` and qualifier `@Default` is registered automatically. Moreover, all templates located in the `src/main/resources/templates` directory are validated and can be easily injected. +NOTE: A valid template file name is a sequence of non-whitespace characters. For example, a template file named `foo and bar.html` will be ignored. + [source,java] ---- import io.quarkus.qute.Engine; diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java index 8c57f269aeff6..fd1604fe59cbf 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -108,6 +108,7 @@ import io.quarkus.qute.ErrorCode; import io.quarkus.qute.Expression; import io.quarkus.qute.Expression.VirtualMethodPart; +import io.quarkus.qute.Identifiers; import io.quarkus.qute.LoopSectionHelper; import io.quarkus.qute.NamespaceResolver; import io.quarkus.qute.ParameterDeclaration; @@ -2180,6 +2181,10 @@ private void scanPathTree(PathTree pathTree, TemplateRootsBuildItem templateRoot if (PathTreeUtils.containsCaseSensitivePath(pathTree, templateRoot)) { pathTree.walkIfContains(templateRoot, visit -> { if (Files.isRegularFile(visit.getPath())) { + if (!Identifiers.isValid(visit.getPath().getFileName().toString())) { + LOGGER.warnf("Invalid file name detected [%s] - template is ignored", visit.getPath()); + return; + } LOGGER.debugf("Found template: %s", visit.getPath()); // remove templateRoot + / final String relativePath = visit.getRelativePath(); diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/identifiers/InvalidTemplateFileNameIgnoredTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/identifiers/InvalidTemplateFileNameIgnoredTest.java new file mode 100644 index 0000000000000..7133694c7a5eb --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/identifiers/InvalidTemplateFileNameIgnoredTest.java @@ -0,0 +1,43 @@ +package io.quarkus.qute.deployment.identifiers; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.logging.LogRecord; + +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.Engine; +import io.quarkus.test.QuarkusUnitTest; + +public class InvalidTemplateFileNameIgnoredTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addAsResource(new StringAsset( + "ignored"), + "templates/foo o.txt")) + .setLogRecordPredicate(log -> log.getLoggerName().contains("QuteProcessor")) + .assertLogRecords(records -> { + for (LogRecord r : records) { + if (r.getMessage().startsWith("Invalid file name detected")) { + return; + } + } + fail(); + }); + + @Inject + Engine engine; + + @Test + public void testTemplateFileIgnored() { + assertFalse(engine.isTemplateLoaded("foo o.txt")); + } + +} diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Engine.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Engine.java index 8018514e85190..eb94cb5f8654e 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Engine.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Engine.java @@ -78,6 +78,7 @@ default Template parse(String content, Variant variant) { String mapResult(Object result, Expression expression); /** + * A valid identifier is a sequence of non-whitespace characters. * * @param id * @param template diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineImpl.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineImpl.java index 7d57aae676ed8..69602e5592ab8 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineImpl.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineImpl.java @@ -117,6 +117,9 @@ public String mapResult(Object result, Expression expression) { } public Template putTemplate(String id, Template template) { + if (!Identifiers.isValid(id)) { + throw new IllegalArgumentException("Invalid identifier found: [" + id + "]"); + } return templates.put(id, template); } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Identifiers.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Identifiers.java new file mode 100644 index 0000000000000..15438f38bc803 --- /dev/null +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Identifiers.java @@ -0,0 +1,28 @@ +package io.quarkus.qute; + +public final class Identifiers { + + /** + * A valid identifier is a sequence of non-whitespace characters. + * + * @param value + * @return {@code true} if the value represents a valid identifier, {@code false} otherwise + */ + public static boolean isValid(String value) { + if (value == null || value.isBlank()) { + return false; + } + int offset = 0; + int length = value.length(); + while (offset < length) { + int c = value.codePointAt(offset); + if (!Character.isWhitespace(c)) { + offset += Character.charCount(c); + continue; + } + return false; + } + return true; + } + +} diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java index ee14ccf58c764..d1f36a32be154 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java @@ -400,20 +400,6 @@ private boolean isValidIdentifierStart(char character) { || Character.isAlphabetic(character); } - static boolean isValidIdentifier(String value) { - int offset = 0; - int length = value.length(); - while (offset < length) { - int c = value.codePointAt(offset); - if (!Character.isWhitespace(c)) { - offset += Character.charCount(c); - continue; - } - return false; - } - return true; - } - private boolean isLineSeparatorStart(char character) { return character == LINE_SEPARATOR_CR || character == LINE_SEPARATOR_LF; } @@ -1084,7 +1070,7 @@ private static Part createPart(Supplier idGenerator, String namespace, .build(); } } else { - if (!isValidIdentifier(value)) { + if (!Identifiers.isValid(value)) { throw error(ParserError.INVALID_IDENTIFIER, "invalid identifier found [{value}]", origin) .argument("value", value) .build(); diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/EngineTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/EngineTest.java index 90e8f085501e4..522434ff95b1d 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/EngineTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/EngineTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -116,4 +117,11 @@ public void accept(TemplateInstance templateInstance) { assertEquals(engine1.getSectionHelperFactories().size(), engine2.getSectionHelperFactories().size()); } + @Test + public void testInvalidTemplateIdentifier() { + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> Engine.builder().build().putTemplate("foo o", null)); + assertEquals("Invalid identifier found: [foo o]", e.getMessage()); + } + } diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/IdentifiersTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/IdentifiersTest.java new file mode 100644 index 0000000000000..1ae73849d820e --- /dev/null +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/IdentifiersTest.java @@ -0,0 +1,21 @@ +package io.quarkus.qute; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +public class IdentifiersTest { + + @Test + public void testIsValid() { + assertTrue(Identifiers.isValid("foo")); + assertTrue(Identifiers.isValid("foo-bar")); + assertTrue(Identifiers.isValid("fíí_")); + assertFalse(Identifiers.isValid("foo bar")); + assertFalse(Identifiers.isValid("")); + assertFalse(Identifiers.isValid(" ")); + assertTrue(Identifiers.isValid("%ů=")); + } + +} diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java index c7375dc68763c..06cebe0e764b2 100644 --- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java @@ -262,16 +262,16 @@ public void testRemoveStandaloneLines() { @Test public void testValidIdentifiers() { - assertTrue(Parser.isValidIdentifier("foo")); - assertTrue(Parser.isValidIdentifier("_foo")); - assertTrue(Parser.isValidIdentifier("foo$$bar")); - assertTrue(Parser.isValidIdentifier("1Foo_$")); - assertTrue(Parser.isValidIdentifier("1")); - assertTrue(Parser.isValidIdentifier("1?")); - assertTrue(Parser.isValidIdentifier("1:")); - assertTrue(Parser.isValidIdentifier("-foo")); - assertTrue(Parser.isValidIdentifier("foo[")); - assertTrue(Parser.isValidIdentifier("foo^")); + assertTrue(Identifiers.isValid("foo")); + assertTrue(Identifiers.isValid("_foo")); + assertTrue(Identifiers.isValid("foo$$bar")); + assertTrue(Identifiers.isValid("1Foo_$")); + assertTrue(Identifiers.isValid("1")); + assertTrue(Identifiers.isValid("1?")); + assertTrue(Identifiers.isValid("1:")); + assertTrue(Identifiers.isValid("-foo")); + assertTrue(Identifiers.isValid("foo[")); + assertTrue(Identifiers.isValid("foo^")); Engine engine = Engine.builder().addDefaults().build(); assertThatExceptionOfType(TemplateException.class) .isThrownBy(() -> engine.parse("{foo\nfoo}"))