From d77474cc37efd8b8385ae3c0ce71ecfee21bbfde Mon Sep 17 00:00:00 2001 From: Pavel Ptashyts <49400901+pavel-ptashyts@users.noreply.github.com> Date: Sat, 7 Nov 2026 20:13:31 +0100 Subject: [PATCH 1/3] Encode form and digest input without CharBuffer StringUtils.charSequence2ByteBuffer ran charset.encode(CharBuffer.wrap(cs)). The wrapped CharBuffer has no backing array, so the encoder walks it char by char and starts from an estimated buffer size. Every url-encoded form body (HttpUtils.urlEncodeFormParams) and every Digest hash input (Realm) goes through it, on the caller thread. charSequence2Bytes now copies an ASCII StringBuilder straight into a byte array for UTF-8, ISO-8859-1 and US-ASCII, which all map a char below U+0080 to that byte. Any other input or charset goes through String.getBytes(charset), which #2371 showed to produce the same bytes as Charset.encode, replacements and byte order marks included. charSequence2ByteBuffer wraps that array. Signatures are unchanged. New tests check the bytes against Charset.encode for every charset the JDK can encode, for String, StringBuilder and CharBuffer input, and on the form and Digest paths with non-ASCII and unpaired surrogates. Claude Code on behalf of Pavel Ptashyts Co-Authored-By: Claude Opus 5.5 --- .../bench/FormDigestEncodeBenchmark.java | 99 +++++++++++++++ .../org/asynchttpclient/util/StringUtils.java | 26 +++- .../java/org/asynchttpclient/RealmTest.java | 32 +++++ .../asynchttpclient/util/HttpUtilsTest.java | 34 ++++++ .../asynchttpclient/util/StringUtilsTest.java | 114 ++++++++++++++++++ 5 files changed, 301 insertions(+), 4 deletions(-) create mode 100644 client/src/jmh/java/org/asynchttpclient/bench/FormDigestEncodeBenchmark.java create mode 100644 client/src/test/java/org/asynchttpclient/util/StringUtilsTest.java diff --git a/client/src/jmh/java/org/asynchttpclient/bench/FormDigestEncodeBenchmark.java b/client/src/jmh/java/org/asynchttpclient/bench/FormDigestEncodeBenchmark.java new file mode 100644 index 0000000000..68d77c50a4 --- /dev/null +++ b/client/src/jmh/java/org/asynchttpclient/bench/FormDigestEncodeBenchmark.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asynchttpclient.bench; + +import org.asynchttpclient.Param; +import org.asynchttpclient.Realm; +import org.asynchttpclient.uri.Uri; +import org.asynchttpclient.util.HttpUtils; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Production paths that encode a pooled StringBuilder to bytes through + * {@code StringUtils.charSequence2ByteBuffer}: an url-encoded form body, and the three + * hashes of a Digest response. Run against two builds to compare them. + */ +@State(Scope.Thread) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class FormDigestEncodeBenchmark { + + private List smallForm; + private List largeForm; + private Realm.Builder asciiDigest; + private Realm.Builder utf8Digest; + + @Setup(Level.Trial) + public void setup() { + smallForm = form(4); + largeForm = form(256); + Uri uri = Uri.create("http://localhost:8080/protected/resource?id=42"); + String challenge = "Digest realm=\"protected\", nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", qop=\"auth\""; + asciiDigest = new Realm.Builder("user", "secret-password") + .setUri(uri) + .setMethodName("GET") + .parseWWWAuthenticateHeader(challenge); + utf8Digest = new Realm.Builder("пользователь", "пароль") + .setUri(uri) + .setMethodName("GET") + .parseWWWAuthenticateHeader(challenge + ", charset=UTF-8"); + } + + private static List form(int params) { + List list = new ArrayList<>(params); + for (int i = 0; i < params; i++) { + list.add(new Param("field" + i, "value " + i + "&x=y")); + } + return list; + } + + /** 4 params, a 91 byte body. */ + @Benchmark + public ByteBuffer formSmall() { + return HttpUtils.urlEncodeFormParams(smallForm, StandardCharsets.UTF_8); + } + + /** 256 params, a 6,691 byte body. */ + @Benchmark + public ByteBuffer formLarge() { + return HttpUtils.urlEncodeFormParams(largeForm, StandardCharsets.UTF_8); + } + + /** ISO-8859-1 digest of ASCII credentials. */ + @Benchmark + public Realm digestAscii() { + return asciiDigest.build(); + } + + /** UTF-8 digest of Cyrillic credentials: HA1 is not ASCII. */ + @Benchmark + public Realm digestUtf8() { + return utf8Digest.build(); + } +} diff --git a/client/src/main/java/org/asynchttpclient/util/StringUtils.java b/client/src/main/java/org/asynchttpclient/util/StringUtils.java index 0abf0b686d..35e202d60b 100644 --- a/client/src/main/java/org/asynchttpclient/util/StringUtils.java +++ b/client/src/main/java/org/asynchttpclient/util/StringUtils.java @@ -16,9 +16,12 @@ package org.asynchttpclient.util; import java.nio.ByteBuffer; -import java.nio.CharBuffer; import java.nio.charset.Charset; +import static java.nio.charset.StandardCharsets.ISO_8859_1; +import static java.nio.charset.StandardCharsets.US_ASCII; +import static java.nio.charset.StandardCharsets.UTF_8; + public final class StringUtils { private StringUtils() { @@ -26,7 +29,7 @@ private StringUtils() { } public static ByteBuffer charSequence2ByteBuffer(CharSequence cs, Charset charset) { - return charset.encode(CharBuffer.wrap(cs)); + return ByteBuffer.wrap(charSequence2Bytes(cs, charset)); } public static byte[] byteBuffer2ByteArray(ByteBuffer bb) { @@ -36,8 +39,23 @@ public static byte[] byteBuffer2ByteArray(ByteBuffer bb) { } public static byte[] charSequence2Bytes(CharSequence sb, Charset charset) { - ByteBuffer bb = charSequence2ByteBuffer(sb, charset); - return byteBuffer2ByteArray(bb); + if (sb instanceof String) { + return ((String) sb).getBytes(charset); + } + // Each of these charsets maps a char below U+0080 to that byte, so ASCII input needs no String copy. + if (charset.equals(UTF_8) || charset.equals(ISO_8859_1) || charset.equals(US_ASCII)) { + int length = sb.length(); + byte[] bytes = new byte[length]; + for (int i = 0; i < length; i++) { + char c = sb.charAt(i); + if (c >= 0x80) { + return sb.toString().getBytes(charset); + } + bytes[i] = (byte) c; + } + return bytes; + } + return sb.toString().getBytes(charset); } public static String toHexString(byte[] data) { diff --git a/client/src/test/java/org/asynchttpclient/RealmTest.java b/client/src/test/java/org/asynchttpclient/RealmTest.java index 590def0a8c..bb62b59a42 100644 --- a/client/src/test/java/org/asynchttpclient/RealmTest.java +++ b/client/src/test/java/org/asynchttpclient/RealmTest.java @@ -16,6 +16,8 @@ import org.asynchttpclient.util.StringUtils; import org.junit.jupiter.api.Test; +import java.nio.CharBuffer; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; @@ -131,6 +133,36 @@ public void testAuthIntDigestKeepsMethodAndUriInA2() throws Exception { assertEquals(expectedResponse, orig.getResponse()); } + @Test + public void digestHashesNonAsciiInputAsCharsetEncodeDoes() throws Exception { + String user = "пользователь"; + String pass = "päss\ud800w€😀"; + String realmName = "réalm"; + String nonce = "nonce"; + String method = "GET"; + Uri uri = Uri.create("http://ahc.io/foo"); + for (String charsetParam : new String[]{"", ", charset=UTF-8"}) { + Realm realm = digestAuthRealm(user, pass) + .setUri(uri) + .setMethodName(method) + .parseWWWAuthenticateHeader("Digest realm=\"" + realmName + "\", nonce=\"" + nonce + "\", qop=\"auth\"" + charsetParam) + .build(); + + Charset charset = realm.getCharset(); + assertEquals(charsetParam.isEmpty() ? StandardCharsets.ISO_8859_1 : StandardCharsets.UTF_8, charset); + String ha1 = charsetEncodeMd5(user + ':' + realmName + ':' + pass, charset); + String ha2 = charsetEncodeMd5(method + ':' + uri.getPath(), charset); + String expectedResponse = charsetEncodeMd5(ha1 + ':' + nonce + ':' + realm.getNc() + ':' + realm.getCnonce() + ":auth:" + ha2, charset); + assertEquals(expectedResponse, realm.getResponse(), charset.name()); + } + } + + private static String charsetEncodeMd5(String what, Charset charset) throws Exception { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(charset.encode(CharBuffer.wrap(what))); + return StringUtils.toHexString(md.digest()); + } + // Phase 1: matchParam tests @Test public void testMatchParamUnquotedAlgorithm() { diff --git a/client/src/test/java/org/asynchttpclient/util/HttpUtilsTest.java b/client/src/test/java/org/asynchttpclient/util/HttpUtilsTest.java index 7ed607dded..424f70b8ba 100644 --- a/client/src/test/java/org/asynchttpclient/util/HttpUtilsTest.java +++ b/client/src/test/java/org/asynchttpclient/util/HttpUtilsTest.java @@ -26,6 +26,7 @@ import java.net.URLEncoder; import java.nio.ByteBuffer; +import java.nio.CharBuffer; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.HashSet; @@ -157,6 +158,39 @@ public void formUrlEncodingShouldSupportNonUtf8Charset() throws Exception { formUrlEncoding(Charset.forName("GBK")); } + @Test + public void formParamBytesMatchCharsetEncodeOfTheEncodedForm() { + List params = new ArrayList<>(); + params.add(new Param("a b", "café")); + params.add(new Param("中文", null)); + params.add(new Param("x😀y", "a\ud800b")); + params.add(new Param("\udc00", "")); + for (Charset charset : new Charset[]{UTF_8, ISO_8859_1, Charset.forName("GBK")}) { + StringBuilder form = new StringBuilder(); + for (Param param : params) { + appendFormField(form, param.getName(), charset); + if (param.getValue() != null) { + appendFormField(form.append('='), param.getValue(), charset); + } + form.append('&'); + } + form.setLength(form.length() - 1); + ByteBuffer expected = US_ASCII.encode(CharBuffer.wrap(form)); + + // the pooled builder keeps a UTF-16 coder once it has held a char above U+00FF + StringBuilderPool.DEFAULT.stringBuilder().append('Ā'); + assertEquals(expected, HttpUtils.urlEncodeFormParams(params, charset), charset.name()); + } + } + + private static void appendFormField(StringBuilder sb, String field, Charset charset) { + if (charset.equals(UTF_8)) { + Utf8UrlEncoder.encodeAndAppendFormElement(sb, field); + } else { + sb.append(URLEncoder.encode(field, charset)); + } + } + @Test public void computeOriginForPlainUriWithImplicitPort() { assertEquals("http://foo.com", HttpUtils.originHeader(Uri.create("ws://foo.com/bar"))); diff --git a/client/src/test/java/org/asynchttpclient/util/StringUtilsTest.java b/client/src/test/java/org/asynchttpclient/util/StringUtilsTest.java new file mode 100644 index 0000000000..9dd6e43193 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/util/StringUtilsTest.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.asynchttpclient.util; + +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link StringUtils#charSequence2Bytes} and {@link StringUtils#charSequence2ByteBuffer} must return + * exactly the bytes {@code charset.encode(CharBuffer.wrap(cs))} produces, including the replacement + * of malformed and unmappable input and the byte order mark of charsets that write one. + */ +public class StringUtilsTest { + + private static final String[] INPUTS = { + "", + "hello", + "café", + "€ 100", + "中文", + "\u0000\u007f\u0080ÿĀ", + // supplementary character (surrogate pair) + "x😀y", + // lone high surrogate at the end, at the start and before another high surrogate + "a\ud800", + "\ud800a", + "\ud800𐀀", + // lone low surrogate, and a low surrogate followed by a high one + "\udc00a", + "a\udc00\ud800b", + "😀\ud800", + // byte order mark and replacement character as content + "bom", + "�", + }; + + @Test + public void bytesMatchCharsetEncodeForEveryEncodingCharset() { + List mismatches = new ArrayList<>(); + for (Charset charset : Charset.availableCharsets().values()) { + if (!charset.canEncode()) { + continue; + } + for (String input : INPUTS) { + for (CharSequence cs : sequences(input)) { + byte[] expected = charsetEncode(cs, charset); + if (!Arrays.equals(expected, StringUtils.charSequence2Bytes(cs, charset)) + || !Arrays.equals(expected, remaining(StringUtils.charSequence2ByteBuffer(cs, charset)))) { + mismatches.add(charset.name() + " / " + cs.getClass().getSimpleName() + " / " + escape(input)); + } + } + } + } + assertTrue(mismatches.isEmpty(), "bytes differ from Charset.encode for " + mismatches); + } + + @Test + public void unpairedSurrogatesBecomeQuestionMarks() { + assertArrayEquals(new byte[]{'a', '?', 'b'}, StringUtils.charSequence2Bytes(new StringBuilder("a\ud800b"), StandardCharsets.UTF_8)); + assertArrayEquals(new byte[]{'?', 'a'}, StringUtils.charSequence2Bytes(new StringBuilder("\udc00a"), StandardCharsets.US_ASCII)); + // a valid pair is one unmappable character in a single byte charset + assertArrayEquals(new byte[]{'x', '?', 'y'}, + remaining(StringUtils.charSequence2ByteBuffer(new StringBuilder("x😀y"), StandardCharsets.ISO_8859_1))); + } + + private static List sequences(String input) { + // a recycled StringBuilder keeps a UTF-16 coder after it once held a char above U+00FF + StringBuilder inflated = new StringBuilder("Ā"); + inflated.setLength(0); + inflated.append(input); + return Arrays.asList(input, new StringBuilder(input), inflated, CharBuffer.wrap(input)); + } + + private static byte[] charsetEncode(CharSequence cs, Charset charset) { + return remaining(charset.encode(CharBuffer.wrap(cs))); + } + + private static byte[] remaining(ByteBuffer bb) { + byte[] bytes = new byte[bb.remaining()]; + bb.duplicate().get(bytes); + return bytes; + } + + private static String escape(String s) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + sb.append(String.format("U+%04X ", (int) s.charAt(i))); + } + return sb.toString().trim(); + } +} From 46ff4250d8d74672a7bbb7ab692bd14d693e3f6d Mon Sep 17 00:00:00 2001 From: Pavel Ptashyts <49400901+pavel-ptashyts@users.noreply.github.com> Date: Sat, 7 Nov 2026 20:24:34 +0100 Subject: [PATCH 2/3] Bench the encoding step alone End-to-end form and Digest timings are dominated by url-encoding and SecureRandom, so the change under test is lost in their noise. Add arms that run only charSequence2ByteBuffer on the builders those paths produce, including a non-ASCII UTF-8 one for the fallback. Claude Code on behalf of Pavel Ptashyts Co-Authored-By: Claude Opus 5.5 --- .../bench/FormDigestEncodeBenchmark.java | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/client/src/jmh/java/org/asynchttpclient/bench/FormDigestEncodeBenchmark.java b/client/src/jmh/java/org/asynchttpclient/bench/FormDigestEncodeBenchmark.java index 68d77c50a4..3faa0a501d 100644 --- a/client/src/jmh/java/org/asynchttpclient/bench/FormDigestEncodeBenchmark.java +++ b/client/src/jmh/java/org/asynchttpclient/bench/FormDigestEncodeBenchmark.java @@ -19,6 +19,7 @@ import org.asynchttpclient.Realm; import org.asynchttpclient.uri.Uri; import org.asynchttpclient.util.HttpUtils; +import org.asynchttpclient.util.StringUtils; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Level; @@ -37,29 +38,43 @@ /** * Production paths that encode a pooled StringBuilder to bytes through * {@code StringUtils.charSequence2ByteBuffer}: an url-encoded form body, and the three - * hashes of a Digest response. Run against two builds to compare them. + * hashes of a Digest response, end to end and the encoding step alone. Run against two builds to + * compare them. */ @State(Scope.Thread) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) public class FormDigestEncodeBenchmark { + private static final String UTF8_USER = "пользователь"; + private static final String UTF8_PASSWORD = "пароль"; + private List smallForm; private List largeForm; private Realm.Builder asciiDigest; private Realm.Builder utf8Digest; + private StringBuilder formBuilder; + private StringBuilder digestBuilder; + private StringBuilder utf8DigestBuilder; @Setup(Level.Trial) public void setup() { smallForm = form(4); largeForm = form(256); + ByteBuffer formBody = HttpUtils.urlEncodeFormParams(largeForm, StandardCharsets.UTF_8); + formBuilder = new StringBuilder(StandardCharsets.US_ASCII.decode(formBody)); + // HA1 ":" nonce ":" nc ":" cnonce ":" qop ":" HA2 + digestBuilder = new StringBuilder("939e7578ed9e3c518a452acee763bce9:dcd98b7102dd2f0e8b11d0f600bfb0c093:00000001:" + + "0a4f113b5c8e1d2a:auth:39aff3a2bab6126f332b942af96d3366"); + // username ":" realm ":" password + utf8DigestBuilder = new StringBuilder(UTF8_USER + ":protected:" + UTF8_PASSWORD); Uri uri = Uri.create("http://localhost:8080/protected/resource?id=42"); String challenge = "Digest realm=\"protected\", nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", qop=\"auth\""; asciiDigest = new Realm.Builder("user", "secret-password") .setUri(uri) .setMethodName("GET") .parseWWWAuthenticateHeader(challenge); - utf8Digest = new Realm.Builder("пользователь", "пароль") + utf8Digest = new Realm.Builder(UTF8_USER, UTF8_PASSWORD) .setUri(uri) .setMethodName("GET") .parseWWWAuthenticateHeader(challenge + ", charset=UTF-8"); @@ -96,4 +111,22 @@ public Realm digestAscii() { public Realm digestUtf8() { return utf8Digest.build(); } + + /** The encoding step of {@link #formLarge} alone. */ + @Benchmark + public ByteBuffer encodeFormBuilder() { + return StringUtils.charSequence2ByteBuffer(formBuilder, StandardCharsets.US_ASCII); + } + + /** The encoding step of the last {@link #digestAscii} hash alone. */ + @Benchmark + public ByteBuffer encodeDigestBuilder() { + return StringUtils.charSequence2ByteBuffer(digestBuilder, StandardCharsets.ISO_8859_1); + } + + /** The encoding step of the first {@link #digestUtf8} hash alone: non-ASCII input. */ + @Benchmark + public ByteBuffer encodeUtf8DigestBuilder() { + return StringUtils.charSequence2ByteBuffer(utf8DigestBuilder, StandardCharsets.UTF_8); + } } From 6657460748eb11ab4167fb5e504c9ca43b9c8669 Mon Sep 17 00:00:00 2001 From: Pavel Ptashyts <49400901+pavel-ptashyts@users.noreply.github.com> Date: Sat, 7 Nov 2026 20:48:02 +0100 Subject: [PATCH 3/3] Escape non-ASCII test and bench literals AGENTS.md requires ASCII-only repository content. The new test and benchmark strings carried raw non-ASCII characters; write them as Java unicode escapes instead. The strings are unchanged. Claude Code on behalf of Pavel Ptashyts Co-Authored-By: Claude Opus 5.5 --- .../bench/FormDigestEncodeBenchmark.java | 4 ++-- .../java/org/asynchttpclient/RealmTest.java | 6 ++--- .../asynchttpclient/util/HttpUtilsTest.java | 8 +++---- .../asynchttpclient/util/StringUtilsTest.java | 22 +++++++++---------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/client/src/jmh/java/org/asynchttpclient/bench/FormDigestEncodeBenchmark.java b/client/src/jmh/java/org/asynchttpclient/bench/FormDigestEncodeBenchmark.java index 3faa0a501d..62185c17de 100644 --- a/client/src/jmh/java/org/asynchttpclient/bench/FormDigestEncodeBenchmark.java +++ b/client/src/jmh/java/org/asynchttpclient/bench/FormDigestEncodeBenchmark.java @@ -46,8 +46,8 @@ @OutputTimeUnit(TimeUnit.NANOSECONDS) public class FormDigestEncodeBenchmark { - private static final String UTF8_USER = "пользователь"; - private static final String UTF8_PASSWORD = "пароль"; + private static final String UTF8_USER = "\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c"; + private static final String UTF8_PASSWORD = "\u043f\u0430\u0440\u043e\u043b\u044c"; private List smallForm; private List largeForm; diff --git a/client/src/test/java/org/asynchttpclient/RealmTest.java b/client/src/test/java/org/asynchttpclient/RealmTest.java index bb62b59a42..93ba122435 100644 --- a/client/src/test/java/org/asynchttpclient/RealmTest.java +++ b/client/src/test/java/org/asynchttpclient/RealmTest.java @@ -135,9 +135,9 @@ public void testAuthIntDigestKeepsMethodAndUriInA2() throws Exception { @Test public void digestHashesNonAsciiInputAsCharsetEncodeDoes() throws Exception { - String user = "пользователь"; - String pass = "päss\ud800w€😀"; - String realmName = "réalm"; + String user = "\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c"; + String pass = "p\u00e4ss\ud800w\u20ac\ud83d\ude00"; + String realmName = "r\u00e9alm"; String nonce = "nonce"; String method = "GET"; Uri uri = Uri.create("http://ahc.io/foo"); diff --git a/client/src/test/java/org/asynchttpclient/util/HttpUtilsTest.java b/client/src/test/java/org/asynchttpclient/util/HttpUtilsTest.java index 424f70b8ba..8bdaf44e90 100644 --- a/client/src/test/java/org/asynchttpclient/util/HttpUtilsTest.java +++ b/client/src/test/java/org/asynchttpclient/util/HttpUtilsTest.java @@ -161,9 +161,9 @@ public void formUrlEncodingShouldSupportNonUtf8Charset() throws Exception { @Test public void formParamBytesMatchCharsetEncodeOfTheEncodedForm() { List params = new ArrayList<>(); - params.add(new Param("a b", "café")); - params.add(new Param("中文", null)); - params.add(new Param("x😀y", "a\ud800b")); + params.add(new Param("a b", "caf\u00e9")); + params.add(new Param("\u4e2d\u6587", null)); + params.add(new Param("x\ud83d\ude00y", "a\ud800b")); params.add(new Param("\udc00", "")); for (Charset charset : new Charset[]{UTF_8, ISO_8859_1, Charset.forName("GBK")}) { StringBuilder form = new StringBuilder(); @@ -178,7 +178,7 @@ public void formParamBytesMatchCharsetEncodeOfTheEncodedForm() { ByteBuffer expected = US_ASCII.encode(CharBuffer.wrap(form)); // the pooled builder keeps a UTF-16 coder once it has held a char above U+00FF - StringBuilderPool.DEFAULT.stringBuilder().append('Ā'); + StringBuilderPool.DEFAULT.stringBuilder().append('\u0100'); assertEquals(expected, HttpUtils.urlEncodeFormParams(params, charset), charset.name()); } } diff --git a/client/src/test/java/org/asynchttpclient/util/StringUtilsTest.java b/client/src/test/java/org/asynchttpclient/util/StringUtilsTest.java index 9dd6e43193..e2cf987c55 100644 --- a/client/src/test/java/org/asynchttpclient/util/StringUtilsTest.java +++ b/client/src/test/java/org/asynchttpclient/util/StringUtilsTest.java @@ -38,23 +38,23 @@ public class StringUtilsTest { private static final String[] INPUTS = { "", "hello", - "café", - "€ 100", - "中文", - "\u0000\u007f\u0080ÿĀ", + "caf\u00e9", + "\u20ac 100", + "\u4e2d\u6587", + "\u0000\u007f\u0080\u00ff\u0100", // supplementary character (surrogate pair) - "x😀y", + "x\ud83d\ude00y", // lone high surrogate at the end, at the start and before another high surrogate "a\ud800", "\ud800a", - "\ud800𐀀", + "\ud800\ud800\udc00", // lone low surrogate, and a low surrogate followed by a high one "\udc00a", "a\udc00\ud800b", - "😀\ud800", + "\ud83d\ude00\ud800", // byte order mark and replacement character as content - "bom", - "�", + "\ufeffbom", + "\ufffd", }; @Test @@ -83,12 +83,12 @@ public void unpairedSurrogatesBecomeQuestionMarks() { assertArrayEquals(new byte[]{'?', 'a'}, StringUtils.charSequence2Bytes(new StringBuilder("\udc00a"), StandardCharsets.US_ASCII)); // a valid pair is one unmappable character in a single byte charset assertArrayEquals(new byte[]{'x', '?', 'y'}, - remaining(StringUtils.charSequence2ByteBuffer(new StringBuilder("x😀y"), StandardCharsets.ISO_8859_1))); + remaining(StringUtils.charSequence2ByteBuffer(new StringBuilder("x\ud83d\ude00y"), StandardCharsets.ISO_8859_1))); } private static List sequences(String input) { // a recycled StringBuilder keeps a UTF-16 coder after it once held a char above U+00FF - StringBuilder inflated = new StringBuilder("Ā"); + StringBuilder inflated = new StringBuilder("\u0100"); inflated.setLength(0); inflated.append(input); return Arrays.asList(input, new StringBuilder(input), inflated, CharBuffer.wrap(input));