From 5a3c228f3b86fd510a3a385ee9b37b65f3de867e Mon Sep 17 00:00:00 2001 From: ruthes00 Date: Thu, 24 Sep 2026 04:35:06 -0400 Subject: [PATCH 1/4] Fixed issue where escaped characters in data of curl causes unbalanced quotes. --- .../protocol/http/curl/BasicCurlParser.java | 55 ++++++-- .../jmeter/curl/BasicCurlParserTest.java | 122 ++++++++++++++++++ 2 files changed, 164 insertions(+), 13 deletions(-) diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/curl/BasicCurlParser.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/curl/BasicCurlParser.java index d01bb6bcd82..e50d653f277 100644 --- a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/curl/BasicCurlParser.java +++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/curl/BasicCurlParser.java @@ -829,50 +829,79 @@ public static String[] translateCommandline(String toProcess) { //no command? no string return new String[0]; } - // parse with a simple finite state machine + // parse with a character-level finite state machine so that + // backslash-escaped quotes inside a quoted token are handled correctly + // (e.g. 'tes\'t' or "tes\"t"). final int normal = 0; final int inQuote = 1; final int inDoubleQuote = 2; int state = normal; - final StringTokenizer tok = new StringTokenizer(toProcess, "\"\' ", true); final ArrayList result = new ArrayList<>(); final StringBuilder current = new StringBuilder(); boolean lastTokenHasBeenQuoted = false; - while (tok.hasMoreTokens()) { - String nextTok = tok.nextToken(); + int i = 0; + final int len = toProcess.length(); + while (i < len) { + char c = toProcess.charAt(i); switch (state) { case inQuote -> { - if ("'".equals(nextTok)) { + if (c == '\\' && i + 1 < len && toProcess.charAt(i + 1) == '\'') { + // escaped single-quote inside single-quoted string + current.append('\''); + i += 2; + } else if (c == '\'') { lastTokenHasBeenQuoted = true; state = normal; + i++; } else { - current.append(nextTok); + current.append(c); + i++; } } case inDoubleQuote -> { - if ("\"".equals(nextTok)) { + if (c == '\\' && i + 1 < len && toProcess.charAt(i + 1) == '"') { + // escaped double-quote inside double-quoted string + current.append('"'); + i += 2; + } else if (c == '"') { lastTokenHasBeenQuoted = true; state = normal; + i++; } else { - current.append(nextTok); + current.append(c); + i++; } } default -> { - if ("'".equals(nextTok)) { + if (c == '\'') { state = inQuote; - } else if ("\"".equals(nextTok)) { + i++; + } else if (c == '"') { state = inDoubleQuote; - } else if (" ".equals(nextTok)) { + i++; + } else if (c == ' ') { if (lastTokenHasBeenQuoted || !current.isEmpty()) { result.add(current.toString()); current.setLength(0); } + lastTokenHasBeenQuoted = false; + i++; + } else if (c == '\\' && i + 1 < len + && (toProcess.charAt(i + 1) == '\r' || toProcess.charAt(i + 1) == '\n')) { + // backslash line-continuation: skip the backslash and the newline + i += 2; + // also skip a following \n if we consumed \r + if (i < len && toProcess.charAt(i) == '\n') { + i++; + } + lastTokenHasBeenQuoted = false; } else { - current.append(nextTok.replaceAll("^\\\\[\\r\\n]", "")); + current.append(c); + lastTokenHasBeenQuoted = false; + i++; } - lastTokenHasBeenQuoted = false; } } } diff --git a/src/protocol/http/src/test/java/org/apache/jmeter/curl/BasicCurlParserTest.java b/src/protocol/http/src/test/java/org/apache/jmeter/curl/BasicCurlParserTest.java index 0efc161cd3a..494918b15d2 100644 --- a/src/protocol/http/src/test/java/org/apache/jmeter/curl/BasicCurlParserTest.java +++ b/src/protocol/http/src/test/java/org/apache/jmeter/curl/BasicCurlParserTest.java @@ -770,4 +770,126 @@ public void testIsValidCookie() { assertTrue(BasicCurlParser.isValidCookie("a=b;c=d"), "The string should be cookies"); assertFalse(BasicCurlParser.isValidCookie("test.txt"), "A filename is not a valid cookie"); } + + /** + * Escaped single-quote inside a single-quoted --data value must not cause + * "unbalanced quotes" and must be included literally in the post data. + * Reproduces https://github.com/apache/jmeter/issues/6374 + */ + @Test + public void testEscapedSingleQuoteInData() { + // Shell representation: --data 'tes\'t' + // In Java string: the outer single-quotes are literal chars, the \' is a backslash + single-quote + String curl = " curl -X POST \"localhost.com\" --data 'tes\\'t'"; + BasicCurlParser basicCurlParser = new BasicCurlParser(); + BasicCurlParser.Request request = basicCurlParser.parse(curl); + assertEquals("tes't", request.getPostData(), + "Escaped single-quote inside single-quoted data should be preserved"); + } + + /** + * Escaped double-quote inside a double-quoted --data value must not cause + * "unbalanced quotes" and must be included literally in the post data. + * Reproduces https://github.com/apache/jmeter/issues/6374 + */ + @Test + public void testEscapedDoubleQuoteInData() { + // Shell representation: --data "tes\"t" + String curl = " curl -X POST \"localhost.com\" --data \"tes\\\"t\""; + BasicCurlParser basicCurlParser = new BasicCurlParser(); + BasicCurlParser.Request request = basicCurlParser.parse(curl); + assertEquals("tes\"t", request.getPostData(), + "Escaped double-quote inside double-quoted data should be preserved"); + } + + // ----------------------------------------------------------------------- + // Direct unit tests for translateCommandline (tokenizer-level coverage) + // ----------------------------------------------------------------------- + + /** Plain unquoted tokens are split on spaces. */ + @Test + public void testTranslateCommandlineSimpleTokens() { + String[] result = BasicCurlParser.translateCommandline("curl -X POST http://example.com"); + assertEquals(4, result.length); + assertEquals("curl", result[0]); + assertEquals("-X", result[1]); + assertEquals("POST", result[2]); + assertEquals("http://example.com", result[3]); + } + + /** Single-quoted token: quotes are stripped, content preserved verbatim. */ + @Test + public void testTranslateCommandlineSingleQuotedToken() { + String[] result = BasicCurlParser.translateCommandline("curl 'hello world'"); + assertEquals(2, result.length); + assertEquals("curl", result[0]); + assertEquals("hello world", result[1]); + } + + /** Double-quoted token: quotes are stripped, content preserved verbatim. */ + @Test + public void testTranslateCommandlineDoubleQuotedToken() { + String[] result = BasicCurlParser.translateCommandline("curl \"hello world\""); + assertEquals(2, result.length); + assertEquals("curl", result[0]); + assertEquals("hello world", result[1]); + } + + /** + * Backslash-escaped single-quote inside a single-quoted token must be + * treated as a literal single-quote (fix for issue #6374). + */ + @Test + public void testTranslateCommandlineEscapedSingleQuoteInsideSingleQuotes() { + // Input string (as seen by the JVM): 'tes\'t' + // i.e. single-quote, t, e, s, backslash, single-quote, t, single-quote + String[] result = BasicCurlParser.translateCommandline("'tes\\'t'"); + assertEquals(1, result.length); + assertEquals("tes't", result[0]); + } + + /** + * Backslash-escaped double-quote inside a double-quoted token must be + * treated as a literal double-quote (fix for issue #6374). + */ + @Test + public void testTranslateCommandlineEscapedDoubleQuoteInsideDoubleQuotes() { + // Input string (as seen by the JVM): "tes\"t" + String[] result = BasicCurlParser.translateCommandline("\"tes\\\"t\""); + assertEquals(1, result.length); + assertEquals("tes\"t", result[0]); + } + + /** Multiple escaped quotes in a single token are all preserved. */ + @Test + public void testTranslateCommandlineMultipleEscapedQuotes() { + // 'it\'s a test\'s value' → it's a test's value + String[] result = BasicCurlParser.translateCommandline("'it\\'s a test\\'s value'"); + assertEquals(1, result.length); + assertEquals("it's a test's value", result[0]); + } + + /** Backslash + newline (line continuation) outside quotes is consumed silently. */ + @Test + public void testTranslateCommandlineBackslashLineContinuation() { + String[] result = BasicCurlParser.translateCommandline("curl \\\n-d 'hey'"); + assertEquals(3, result.length); + assertEquals("curl", result[0]); + assertEquals("-d", result[1]); + assertEquals("hey", result[2]); + } + + /** Empty input returns an empty array. */ + @Test + public void testTranslateCommandlineEmptyInput() { + assertEquals(0, BasicCurlParser.translateCommandline("").length); + } + + /** Genuinely unbalanced quotes still throw IllegalArgumentException. */ + @Test + public void testTranslateCommandlineUnbalancedQuotesStillThrows() { + assertThrows(IllegalArgumentException.class, + () -> BasicCurlParser.translateCommandline("curl \"unclosed"), + "Genuinely unbalanced quotes must still throw"); + } } From 9a673b4d9c40d0c01b51f4c5b469fccb86368f2a Mon Sep 17 00:00:00 2001 From: ruthes00 Date: Thu, 24 Sep 2026 10:41:34 -0400 Subject: [PATCH 2/4] Fixed build break where the action gradle/actions/wrapper-validation is not allowed in apache/jmeter because all actions must be from a repository owned by your enterprise, created by GitHub, or match one of the patterns. --- .github/workflows/gradle-wrapper-validation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index 78ee9d201e9..0910c38fd77 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -7,4 +7,4 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: gradle/actions/wrapper-validation@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + - uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 From cfc9338568f033a075eb867af6aab0b08f7790a2 Mon Sep 17 00:00:00 2001 From: ruthes00 Date: Fri, 25 Sep 2026 02:43:38 -0400 Subject: [PATCH 3/4] Implemented changes to code and tests in response to maintainer feedback of PR. --- .../workflows/gradle-wrapper-validation.yml | 2 +- .../protocol/http/curl/BasicCurlParser.java | 70 +++++-- .../jmeter/curl/BasicCurlParserTest.java | 197 ++++++++++-------- xdocs/changes.xml | 1 + 4 files changed, 163 insertions(+), 107 deletions(-) diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index 0910c38fd77..78ee9d201e9 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -7,4 +7,4 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 + - uses: gradle/actions/wrapper-validation@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/curl/BasicCurlParser.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/curl/BasicCurlParser.java index e50d653f277..d84325cc3c4 100644 --- a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/curl/BasicCurlParser.java +++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/curl/BasicCurlParser.java @@ -818,7 +818,25 @@ public Request parse(String commandLine) { } /** - * Crack a command line. + * Break a command line into an array of arguments, using shell-like quoting rules: + *
    + *
  • Tokens are delimited by unquoted spaces.
  • + *
  • Single-quoted strings ({@code '...'}) preserve every character literally, + * including backslashes. Nothing can be escaped inside single quotes; the + * first {@code '} ends the quoted region.
  • + *
  • Double-quoted strings ({@code "..."}) follow POSIX rules: a backslash + * is an escape character only before {@code "}, {@code \}, {@code $}, + * `, and a newline; before any other character the + * backslash is kept as a literal {@code \}.
  • + *
  • Outside of quotes, a backslash escapes the immediately following + * character: the backslash is dropped and the next character is appended + * literally. The one exception is a backslash followed by {@code }, + * which is treated as a line-continuation and discards both characters. + * A backslash followed by {@code } escapes the carriage-return + * character itself (appending it to the current token).
  • + *
  • ANSI-C quoting ({@code $'...'}) is not supported; an + * {@link IllegalArgumentException} is thrown if it is encountered.
  • + *
* * @param toProcess the command line to process. * @return the command line broken into strings. @@ -829,9 +847,6 @@ public static String[] translateCommandline(String toProcess) { //no command? no string return new String[0]; } - // parse with a character-level finite state machine so that - // backslash-escaped quotes inside a quoted token are handled correctly - // (e.g. 'tes\'t' or "tes\"t"). final int normal = 0; final int inQuote = 1; @@ -847,11 +862,7 @@ public static String[] translateCommandline(String toProcess) { char c = toProcess.charAt(i); switch (state) { case inQuote -> { - if (c == '\\' && i + 1 < len && toProcess.charAt(i + 1) == '\'') { - // escaped single-quote inside single-quoted string - current.append('\''); - i += 2; - } else if (c == '\'') { + if (c == '\'') { lastTokenHasBeenQuoted = true; state = normal; i++; @@ -861,10 +872,22 @@ public static String[] translateCommandline(String toProcess) { } } case inDoubleQuote -> { - if (c == '\\' && i + 1 < len && toProcess.charAt(i + 1) == '"') { - // escaped double-quote inside double-quoted string - current.append('"'); - i += 2; + if (c == '\\' && i + 1 < len) { + char next = toProcess.charAt(i + 1); + if (next == '"' || next == '\\' || next == '$' || next == '`' || next == '\n') { + current.append(next); + i += 2; + } else if (next == '\r') { + // backslash-newline line continuation inside double quotes + i += 2; + if (i < len && toProcess.charAt(i) == '\n') { + i++; + } + } else { + // backslash is literal before any other character + current.append(c); + i++; + } } else if (c == '"') { lastTokenHasBeenQuoted = true; state = normal; @@ -875,7 +898,10 @@ public static String[] translateCommandline(String toProcess) { } } default -> { - if (c == '\'') { + if (c == '$' && i + 1 < len && toProcess.charAt(i + 1) == '\'') { + throw new IllegalArgumentException( + "ANSI-C quoting ($'...') is not supported in: " + toProcess); + } else if (c == '\'') { state = inQuote; i++; } else if (c == '"') { @@ -888,13 +914,15 @@ public static String[] translateCommandline(String toProcess) { } lastTokenHasBeenQuoted = false; i++; - } else if (c == '\\' && i + 1 < len - && (toProcess.charAt(i + 1) == '\r' || toProcess.charAt(i + 1) == '\n')) { - // backslash line-continuation: skip the backslash and the newline - i += 2; - // also skip a following \n if we consumed \r - if (i < len && toProcess.charAt(i) == '\n') { - i++; + } else if (c == '\\' && i + 1 < len) { + char next = toProcess.charAt(i + 1); + if (next == '\n') { + // backslash-LF line continuation: discard both + i += 2; + } else { + // backslash escapes any other character literally (including \r) + current.append(next); + i += 2; } lastTokenHasBeenQuoted = false; } else { diff --git a/src/protocol/http/src/test/java/org/apache/jmeter/curl/BasicCurlParserTest.java b/src/protocol/http/src/test/java/org/apache/jmeter/curl/BasicCurlParserTest.java index 494918b15d2..99586057f32 100644 --- a/src/protocol/http/src/test/java/org/apache/jmeter/curl/BasicCurlParserTest.java +++ b/src/protocol/http/src/test/java/org/apache/jmeter/curl/BasicCurlParserTest.java @@ -772,19 +772,19 @@ public void testIsValidCookie() { } /** - * Escaped single-quote inside a single-quoted --data value must not cause - * "unbalanced quotes" and must be included literally in the post data. - * Reproduces https://github.com/apache/jmeter/issues/6374 + * A single quote inside a value can be written using the POSIX idiom + * {@code 'tes'\''t'}: close the single-quoted region, escape the single + * quote with a backslash outside quotes, then reopen single-quoting. */ @Test public void testEscapedSingleQuoteInData() { - // Shell representation: --data 'tes\'t' - // In Java string: the outer single-quotes are literal chars, the \' is a backslash + single-quote - String curl = " curl -X POST \"localhost.com\" --data 'tes\\'t'"; + // Shell representation: --data 'tes'\''t' + // In Java string: 'tes'\'t' (close quote, backslash+quote outside, reopen quote) + String curl = " curl -X POST \"localhost.com\" --data 'tes'\\''t'"; BasicCurlParser basicCurlParser = new BasicCurlParser(); BasicCurlParser.Request request = basicCurlParser.parse(curl); assertEquals("tes't", request.getPostData(), - "Escaped single-quote inside single-quoted data should be preserved"); + "POSIX single-quote idiom 'tes'\\''t' should produce tes't"); } /** @@ -802,81 +802,99 @@ public void testEscapedDoubleQuoteInData() { "Escaped double-quote inside double-quoted data should be preserved"); } - // ----------------------------------------------------------------------- - // Direct unit tests for translateCommandline (tokenizer-level coverage) - // ----------------------------------------------------------------------- - - /** Plain unquoted tokens are split on spaces. */ - @Test - public void testTranslateCommandlineSimpleTokens() { - String[] result = BasicCurlParser.translateCommandline("curl -X POST http://example.com"); - assertEquals(4, result.length); - assertEquals("curl", result[0]); - assertEquals("-X", result[1]); - assertEquals("POST", result[2]); - assertEquals("http://example.com", result[3]); - } - - /** Single-quoted token: quotes are stripped, content preserved verbatim. */ - @Test - public void testTranslateCommandlineSingleQuotedToken() { - String[] result = BasicCurlParser.translateCommandline("curl 'hello world'"); - assertEquals(2, result.length); - assertEquals("curl", result[0]); - assertEquals("hello world", result[1]); - } - - /** Double-quoted token: quotes are stripped, content preserved verbatim. */ - @Test - public void testTranslateCommandlineDoubleQuotedToken() { - String[] result = BasicCurlParser.translateCommandline("curl \"hello world\""); - assertEquals(2, result.length); - assertEquals("curl", result[0]); - assertEquals("hello world", result[1]); - } - - /** - * Backslash-escaped single-quote inside a single-quoted token must be - * treated as a literal single-quote (fix for issue #6374). - */ - @Test - public void testTranslateCommandlineEscapedSingleQuoteInsideSingleQuotes() { - // Input string (as seen by the JVM): 'tes\'t' - // i.e. single-quote, t, e, s, backslash, single-quote, t, single-quote - String[] result = BasicCurlParser.translateCommandline("'tes\\'t'"); - assertEquals(1, result.length); - assertEquals("tes't", result[0]); - } - - /** - * Backslash-escaped double-quote inside a double-quoted token must be - * treated as a literal double-quote (fix for issue #6374). - */ - @Test - public void testTranslateCommandlineEscapedDoubleQuoteInsideDoubleQuotes() { - // Input string (as seen by the JVM): "tes\"t" - String[] result = BasicCurlParser.translateCommandline("\"tes\\\"t\""); - assertEquals(1, result.length); - assertEquals("tes\"t", result[0]); - } - - /** Multiple escaped quotes in a single token are all preserved. */ - @Test - public void testTranslateCommandlineMultipleEscapedQuotes() { - // 'it\'s a test\'s value' → it's a test's value - String[] result = BasicCurlParser.translateCommandline("'it\\'s a test\\'s value'"); - assertEquals(1, result.length); - assertEquals("it's a test's value", result[0]); - } - - /** Backslash + newline (line continuation) outside quotes is consumed silently. */ - @Test - public void testTranslateCommandlineBackslashLineContinuation() { - String[] result = BasicCurlParser.translateCommandline("curl \\\n-d 'hey'"); - assertEquals(3, result.length); - assertEquals("curl", result[0]); - assertEquals("-d", result[1]); - assertEquals("hey", result[2]); + static java.util.stream.Stream translateCommandlineCases() { + return java.util.stream.Stream.of( + // Plain unquoted tokens split on spaces + // bash: printf "%s\n" curl -X POST http://example.com + // → curl / -X / POST / http://example.com + org.junit.jupiter.params.provider.Arguments.of( + "plain unquoted tokens", + "curl -X POST http://example.com", + new String[]{"curl", "-X", "POST", "http://example.com"}), + + // Single-quoted token: quotes stripped, content verbatim + // bash: printf "%s\n" curl 'hello world' → curl / hello world + org.junit.jupiter.params.provider.Arguments.of( + "single-quoted token with space", + "curl 'hello world'", + new String[]{"curl", "hello world"}), + + // Double-quoted token: quotes stripped, content verbatim + // bash: printf "%s\n" curl "hello world" → curl / hello world + org.junit.jupiter.params.provider.Arguments.of( + "double-quoted token with space", + "curl \"hello world\"", + new String[]{"curl", "hello world"}), + + // POSIX idiom for single quote inside single-quoted string: 'tes'\''t' + // bash: printf "%s\n" 'tes'\''t' → tes't + org.junit.jupiter.params.provider.Arguments.of( + "single quote via POSIX idiom 'tes'\\''t'", + "'tes'\\''t'", + new String[]{"tes't"}), + + // Escaped double-quote inside double-quoted string + // bash: printf "%s\n" "tes\"t" → tes"t + org.junit.jupiter.params.provider.Arguments.of( + "escaped double-quote inside double quotes", + "\"tes\\\"t\"", + new String[]{"tes\"t"}), + + // Multiple POSIX single-quote idioms in one token + // bash: printf "%s\n" 'it'\''s a test'\''s value' → it's a test's value + org.junit.jupiter.params.provider.Arguments.of( + "multiple single quotes via POSIX idiom", + "'it'\\''s a test'\\''s value'", + new String[]{"it's a test's value"}), + + // Backslash + LF line continuation outside quotes + // bash: printf "%s\n" curl \-d 'hey' → curl / -d / hey + org.junit.jupiter.params.provider.Arguments.of( + "backslash-LF line continuation", + "curl \\\n-d 'hey'", + new String[]{"curl", "-d", "hey"}), + + // Backslash + CRLF outside quotes: only \ is a line continuation. + // \ escapes the CR (appending it to the current token); the following + // is not a token separator in this tokenizer, so it is also appended. + // Result: the second token is "\r\n-d" (CR + LF + "-d" run together). + org.junit.jupiter.params.provider.Arguments.of( + "backslash-CRLF: only LF continuation is supported; CR and LF are appended", + "curl \\\r\n-d 'hey'", + new String[]{"curl", "\r\n-d", "hey"}), + + // Backslash inside single quotes is literal; 'C:\dir\' is a complete + // single-quoted string containing C:\dir\ + // bash: set -- curl -d 'C:\dir\'; echo $# → 3 tokens: curl / -d / C:\dir\ + org.junit.jupiter.params.provider.Arguments.of( + "backslash before closing single quote is literal inside single quotes", + "curl -d 'C:\\dir\\'", + new String[]{"curl", "-d", "C:\\dir\\"}), + + // Inside double quotes, \\ → single backslash; closing " is unescaped + // bash: printf "%s\n" curl -d "C:\\dir\\" http://x → curl / -d / C:\dir\ / http://x + org.junit.jupiter.params.provider.Arguments.of( + "escaped backslashes inside double quotes", + "curl -d \"C:\\\\dir\\\\\" http://x", + new String[]{"curl", "-d", "C:\\dir\\", "http://x"}), + + // Inside double quotes, \\ → single backslash + // bash: printf "%s\n" "a\\b" → a\b + org.junit.jupiter.params.provider.Arguments.of( + "double-quoted escaped backslash yields single backslash", + "\"a\\\\b\"", + new String[]{"a\\b"}) + ); + } + + @org.junit.jupiter.params.ParameterizedTest(name = "{0}") + @org.junit.jupiter.params.provider.MethodSource("translateCommandlineCases") + public void testTranslateCommandline(String name, String input, String[] expected) { + String[] result = BasicCurlParser.translateCommandline(input); + assertEquals(expected.length, result.length, "token count for: " + input); + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], result[i], "token[" + i + "] for: " + input); + } } /** Empty input returns an empty array. */ @@ -885,11 +903,20 @@ public void testTranslateCommandlineEmptyInput() { assertEquals(0, BasicCurlParser.translateCommandline("").length); } - /** Genuinely unbalanced quotes still throw IllegalArgumentException. */ + /** Unbalanced double quotes throw IllegalArgumentException. */ @Test - public void testTranslateCommandlineUnbalancedQuotesStillThrows() { + public void testTranslateCommandlineUnbalancedDoubleQuotesThrows() { assertThrows(IllegalArgumentException.class, () -> BasicCurlParser.translateCommandline("curl \"unclosed"), - "Genuinely unbalanced quotes must still throw"); + "Unbalanced double quotes must throw"); } + + /** Unbalanced single quotes throw IllegalArgumentException. */ + @Test + public void testTranslateCommandlineUnbalancedSingleQuotesThrows() { + assertThrows(IllegalArgumentException.class, + () -> BasicCurlParser.translateCommandline("curl 'unclosed"), + "Unbalanced single quotes must throw"); + } + } diff --git a/xdocs/changes.xml b/xdocs/changes.xml index 56b5657563c..386f3524822 100644 --- a/xdocs/changes.xml +++ b/xdocs/changes.xml @@ -126,6 +126,7 @@ Summary
  • 5937Remove deprecated Log4j package scanning and configure plugin metadata processing to improve startup time and avoid deprecation warnings. Contributed by Piotr P. Karwasz (github.com/piotrgithub)
  • 6620Fix report generation paths so dashboard output files are created in the correct location after internal refactoring.
  • 6456Handle malformed percent-encoded URLs gracefully when recording HTTP traffic, logging a warning instead of failing the recording.
  • +
  • 6773Handle escaped characters in data of curl commands.
  • From a56e515e66d5c6ba5fb3999f97f422863dd8e249 Mon Sep 17 00:00:00 2001 From: ruthes00 Date: Fri, 25 Sep 2026 11:38:16 -0400 Subject: [PATCH 4/4] Converted org.junit.jupiter.params.provider.Arguments to static import. --- .../jmeter/curl/BasicCurlParserTest.java | 46 ++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/src/protocol/http/src/test/java/org/apache/jmeter/curl/BasicCurlParserTest.java b/src/protocol/http/src/test/java/org/apache/jmeter/curl/BasicCurlParserTest.java index 99586057f32..1cdff6a87c0 100644 --- a/src/protocol/http/src/test/java/org/apache/jmeter/curl/BasicCurlParserTest.java +++ b/src/protocol/http/src/test/java/org/apache/jmeter/curl/BasicCurlParserTest.java @@ -21,6 +21,7 @@ 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.params.provider.Arguments.of; import java.io.File; import java.io.IOException; @@ -31,6 +32,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.stream.Stream; import org.apache.jmeter.protocol.http.control.Cookie; import org.apache.jmeter.protocol.http.curl.ArgumentHolder; @@ -40,6 +42,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; public class BasicCurlParserTest { @@ -802,55 +807,48 @@ public void testEscapedDoubleQuoteInData() { "Escaped double-quote inside double-quoted data should be preserved"); } - static java.util.stream.Stream translateCommandlineCases() { - return java.util.stream.Stream.of( + static Stream translateCommandlineCases() { + return Stream.of( // Plain unquoted tokens split on spaces // bash: printf "%s\n" curl -X POST http://example.com // → curl / -X / POST / http://example.com - org.junit.jupiter.params.provider.Arguments.of( - "plain unquoted tokens", + of("plain unquoted tokens", "curl -X POST http://example.com", new String[]{"curl", "-X", "POST", "http://example.com"}), // Single-quoted token: quotes stripped, content verbatim // bash: printf "%s\n" curl 'hello world' → curl / hello world - org.junit.jupiter.params.provider.Arguments.of( - "single-quoted token with space", + of("single-quoted token with space", "curl 'hello world'", new String[]{"curl", "hello world"}), // Double-quoted token: quotes stripped, content verbatim // bash: printf "%s\n" curl "hello world" → curl / hello world - org.junit.jupiter.params.provider.Arguments.of( - "double-quoted token with space", + of("double-quoted token with space", "curl \"hello world\"", new String[]{"curl", "hello world"}), // POSIX idiom for single quote inside single-quoted string: 'tes'\''t' // bash: printf "%s\n" 'tes'\''t' → tes't - org.junit.jupiter.params.provider.Arguments.of( - "single quote via POSIX idiom 'tes'\\''t'", + of("single quote via POSIX idiom 'tes'\\''t'", "'tes'\\''t'", new String[]{"tes't"}), // Escaped double-quote inside double-quoted string // bash: printf "%s\n" "tes\"t" → tes"t - org.junit.jupiter.params.provider.Arguments.of( - "escaped double-quote inside double quotes", + of("escaped double-quote inside double quotes", "\"tes\\\"t\"", new String[]{"tes\"t"}), // Multiple POSIX single-quote idioms in one token // bash: printf "%s\n" 'it'\''s a test'\''s value' → it's a test's value - org.junit.jupiter.params.provider.Arguments.of( - "multiple single quotes via POSIX idiom", + of("multiple single quotes via POSIX idiom", "'it'\\''s a test'\\''s value'", new String[]{"it's a test's value"}), // Backslash + LF line continuation outside quotes // bash: printf "%s\n" curl \-d 'hey' → curl / -d / hey - org.junit.jupiter.params.provider.Arguments.of( - "backslash-LF line continuation", + of("backslash-LF line continuation", "curl \\\n-d 'hey'", new String[]{"curl", "-d", "hey"}), @@ -858,37 +856,33 @@ static java.util.stream.Stream tran // \ escapes the CR (appending it to the current token); the following // is not a token separator in this tokenizer, so it is also appended. // Result: the second token is "\r\n-d" (CR + LF + "-d" run together). - org.junit.jupiter.params.provider.Arguments.of( - "backslash-CRLF: only LF continuation is supported; CR and LF are appended", + of("backslash-CRLF: only LF continuation is supported; CR and LF are appended", "curl \\\r\n-d 'hey'", new String[]{"curl", "\r\n-d", "hey"}), // Backslash inside single quotes is literal; 'C:\dir\' is a complete // single-quoted string containing C:\dir\ // bash: set -- curl -d 'C:\dir\'; echo $# → 3 tokens: curl / -d / C:\dir\ - org.junit.jupiter.params.provider.Arguments.of( - "backslash before closing single quote is literal inside single quotes", + of("backslash before closing single quote is literal inside single quotes", "curl -d 'C:\\dir\\'", new String[]{"curl", "-d", "C:\\dir\\"}), // Inside double quotes, \\ → single backslash; closing " is unescaped // bash: printf "%s\n" curl -d "C:\\dir\\" http://x → curl / -d / C:\dir\ / http://x - org.junit.jupiter.params.provider.Arguments.of( - "escaped backslashes inside double quotes", + of("escaped backslashes inside double quotes", "curl -d \"C:\\\\dir\\\\\" http://x", new String[]{"curl", "-d", "C:\\dir\\", "http://x"}), // Inside double quotes, \\ → single backslash // bash: printf "%s\n" "a\\b" → a\b - org.junit.jupiter.params.provider.Arguments.of( - "double-quoted escaped backslash yields single backslash", + of("double-quoted escaped backslash yields single backslash", "\"a\\\\b\"", new String[]{"a\\b"}) ); } - @org.junit.jupiter.params.ParameterizedTest(name = "{0}") - @org.junit.jupiter.params.provider.MethodSource("translateCommandlineCases") + @ParameterizedTest(name = "{0}") + @MethodSource("translateCommandlineCases") public void testTranslateCommandline(String name, String input, String[] expected) { String[] result = BasicCurlParser.translateCommandline(input); assertEquals(expected.length, result.length, "token count for: " + input);