diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java index 2c3c0977362b8..e80081f89f14f 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java @@ -306,6 +306,13 @@ static String missingInRuntime(CamelCatalog catalog, String scheme) { static final Set FILE_SCHEMES = Set.of("file", "ftp", "ftps", "sftp", "file-watch", "smb"); + /** + * A doubled backslash before a character that a single backslash would escape in a regex (\\. \\d \\( ...): the + * user meant the escape. A doubled backslash before any other character (\\myfile) is left alone: \myfile is not a + * regex escape, so a literal backslash is the only thing it can mean. + */ + static final Pattern DOUBLED_BACKSLASH_ESCAPE = Pattern.compile("\\\\\\\\[.dswDSWbB()\\[\\]{}+*?|^$]"); + /** * include and exclude on the file components are regular expressions: include=*.txt fails at startup with a * PatternSyntaxException wrapped in a binding error. Says to write .*\\.txt or use antInclude. @@ -326,6 +333,15 @@ static void checkRegexOptions(List errors, String fullUri, int uriLineId if (!name.equals("include") && !name.equals("exclude") || value.startsWith("{{")) { continue; } + if (DOUBLED_BACKSLASH_ESCAPE.matcher(value).find()) { + // '.*\\.json$' in single quotes: YAML keeps both backslashes, and in a regex \\ is one literal + // backslash, so the pattern matches a file name with a backslash in it: no file matches and the route + // runs in silence (CAMEL-24854) + errors.add(linePrefix(optionLineMap.getOrDefault(name, uriLineIdx)) + fullUri.substring(0, colon) + ": " + + name + "=" + value + " matches a literal backslash in the file name (in a regex \\\\ is one" + + " backslash and \\. is a dot): write " + name + "='" + value.replace("\\\\", "\\") + "'"); + continue; + } try { Pattern.compile(value); } catch (java.util.regex.PatternSyntaxException e) { diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlLines.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlLines.java index 0f245b07c3d7e..9d3f43da61de6 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlLines.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlLines.java @@ -19,6 +19,7 @@ import java.util.regex.Pattern; import org.apache.camel.util.StringHelper; +import org.apache.camel.util.json.Jsoner; /** * Line-level helpers over a YAML source shared by the checks of {@link SourceValidator}: the enclosing EIP of a line, @@ -157,9 +158,23 @@ static boolean isBlockScalarIndicator(String val) { && val.substring(1).chars().allMatch(c -> c == '-' || c == '+' || Character.isDigit(c)); } + /** + * The value of a quoted scalar: inside double quotes YAML reads \\ as one backslash and \" as a quote (so + * ".*\\.pdf" is the regex .*\.pdf), inside single quotes a backslash is a backslash. + */ static String unquote(String val) { if (val.length() >= 2 && val.startsWith("\"") && val.endsWith("\"")) { - return val.substring(1, val.length() - 1); + String inner = val.substring(1, val.length() - 1); + if (inner.indexOf('\\') < 0) { + return inner; + } + try { + // the JSON escapes are the YAML ones that matter here (\\ \" \n \t and unicode) + return Jsoner.unescape(inner); + } catch (RuntimeException e) { + // a YAML-only escape such as \e or \x41: the text as written + return inner; + } } if (val.length() >= 2 && val.startsWith("'") && val.endsWith("'")) { return val.substring(1, val.length() - 1); diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java index 767162c3e1921..88e23e0c18f6a 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java @@ -592,4 +592,35 @@ void aDynamicDirectoryOnAFileEndpointSaysToUseFileNameOrToD() { assertThat(SourceValidator.validateYamlEndpoints(fromYaml, catalog)) .anyMatch(e -> e.startsWith("Line 3: file: the directory archived/${header.monthDir} cannot be dynamic")); } + + /** CAMEL-24854: a doubled backslash in an include regex (kept as is inside single quotes) matches no file. */ + @Test + void aDoubledBackslashInAnIncludeRegexIsReported() { + String yaml = """ + - route: + from: + uri: file:orders + parameters: + include: '.*\\\\.json$' + steps: + - to: + uri: log:done + """; + List errors = SourceValidator.validateYamlEndpoints(yaml, catalog); + assertThat(errors) + .anyMatch(e -> e.startsWith("Line 5: file: include=.*\\\\.json$ matches a literal backslash in the file name") + && e.endsWith("write include='.*\\.json$'")); + + List ok = SourceValidator.validateYamlEndpoints(yaml.replace("\\\\.json", "\\.json"), catalog); + assertThat(ok).noneMatch(e -> e.contains("backslash")); + + // in double quotes YAML reads \\ as one backslash: ".*\\.json$" is the regex .*\.json$, nothing to report + List doubleQuoted + = SourceValidator.validateYamlEndpoints(yaml.replace("'.*\\\\.json$'", "\".*\\\\.json$\""), catalog); + assertThat(doubleQuoted).noneMatch(e -> e.contains("backslash")); + + // \\myfile is a backslash on purpose: \myfile is not a regex escape, so there is nothing else it can mean + List literal = SourceValidator.validateYamlEndpoints(yaml.replace(".*\\\\.json$", ".*\\\\myfile.*"), catalog); + assertThat(literal).noneMatch(e -> e.contains("backslash")); + } }