From 3d3a15b22a20a3bcfbb0f2b37af5933c3a714999 Mon Sep 17 00:00:00 2001 From: Oleg Kalnichevski Date: Fri, 21 Aug 2026 12:47:36 +0200 Subject: [PATCH 1/2] PercentCodec decoder to tolerate non-ASCII characters in input --- .../org/apache/hc/core5/net/PercentCodec.java | 35 ++++++++++++++----- .../apache/hc/core5/net/TestPercentCodec.java | 8 +++++ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/httpcore5/src/main/java/org/apache/hc/core5/net/PercentCodec.java b/httpcore5/src/main/java/org/apache/hc/core5/net/PercentCodec.java index 3e1700002..e63175e90 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/net/PercentCodec.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/net/PercentCodec.java @@ -30,6 +30,7 @@ import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; import java.nio.charset.StandardCharsets; import java.util.BitSet; @@ -219,6 +220,8 @@ static String decode(final CharSequence content, final Charset charset, final bo } final ByteBuffer bb = ByteBuffer.allocate(content.length()); final CharBuffer cb = CharBuffer.wrap(content); + final CharsetDecoder charsetDecoder = (charset != null ? charset : StandardCharsets.UTF_8).newDecoder(); + final CharBuffer result = CharBuffer.allocate(content.length()); while (cb.hasRemaining()) { final char c = cb.get(); if (c == '%' && cb.remaining() >= 2) { @@ -229,18 +232,34 @@ static String decode(final CharSequence content, final Charset charset, final bo if (u != -1 && l != -1) { bb.put((byte) ((u << 4) + l)); } else { - bb.put((byte) '%'); - bb.put((byte) uc); - bb.put((byte) lc); + flushBin(bb, result, charsetDecoder); + result.put('%'); + result.put(uc); + result.put(lc); } - } else if (plusAsBlank && c == '+') { - bb.put((byte) ' '); } else { - bb.put((byte) c); + flushBin(bb, result, charsetDecoder); + if (plusAsBlank && c == '+') { + result.put(' '); + } else { + result.put(c); + } } } - bb.flip(); - return (charset != null ? charset : StandardCharsets.UTF_8).decode(bb).toString(); + flushBin(bb, result, charsetDecoder); + result.flip(); + return result.toString(); + } + + static void flushBin(final ByteBuffer bb, final CharBuffer result, final CharsetDecoder charsetDecoder) { + if (bb == null) { + return; + } + if (bb.position() > 0) { + bb.flip(); + charsetDecoder.decode(bb, result, true); + bb.compact(); + } } public static String decode(final CharSequence content, final Charset charset) { diff --git a/httpcore5/src/test/java/org/apache/hc/core5/net/TestPercentCodec.java b/httpcore5/src/test/java/org/apache/hc/core5/net/TestPercentCodec.java index 7c1b72aea..aa274fbd9 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/net/TestPercentCodec.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/net/TestPercentCodec.java @@ -116,4 +116,12 @@ void testPercentCodecEncodeIsNotRfc7639Canonical() { assertEquals("%7C", PercentCodec.encode("|", StandardCharsets.UTF_8)); } + @Test + void decodeIllegalChars() { + final String s = "1\u012E2\u012E3\u012E4"; + assertEquals(s, PercentCodec.decode(s, StandardCharsets.UTF_8)); + assertEquals(s, PercentCodec.decode("1\u012E2%C4%AE3\u012E4", StandardCharsets.UTF_8)); + assertEquals(s, PercentCodec.decode("1%C4%AE2%C4%AE3%C4%AE4", StandardCharsets.UTF_8)); + } + } From 4ef802c001aeef1f2fc86441993a7bb84f7634c3 Mon Sep 17 00:00:00 2001 From: Oleg Kalnichevski Date: Fri, 21 Aug 2026 12:25:07 +0200 Subject: [PATCH 2/2] PercentCodec encode / decode optimization --- .../org/apache/hc/core5/net/PercentCodec.java | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/httpcore5/src/main/java/org/apache/hc/core5/net/PercentCodec.java b/httpcore5/src/main/java/org/apache/hc/core5/net/PercentCodec.java index e63175e90..e3bc31ac3 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/net/PercentCodec.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/net/PercentCodec.java @@ -180,7 +180,22 @@ static void encode(final StringBuilder buf, final CharSequence content, final Ch return; } final CharBuffer cb = CharBuffer.wrap(content); - final ByteBuffer bb = (charset != null ? charset : StandardCharsets.UTF_8).encode(cb); + ByteBuffer bb = null; + while (cb.hasRemaining()) { + final char ch = cb.get(); + if (safechars.get(ch)) { + buf.append(ch); + } else if (blankAsPlus && ch == ' ') { + buf.append("+"); + } else { + cb.position(cb.position() - 1); + bb = (charset != null ? charset : StandardCharsets.UTF_8).encode(cb); + break; + } + } + if (bb == null) { + return; + } while (bb.hasRemaining()) { final int b = bb.get() & 0xff; if (safechars.get(b)) { @@ -218,10 +233,10 @@ static String decode(final CharSequence content, final Charset charset, final bo if (content == null) { return null; } - final ByteBuffer bb = ByteBuffer.allocate(content.length()); final CharBuffer cb = CharBuffer.wrap(content); - final CharsetDecoder charsetDecoder = (charset != null ? charset : StandardCharsets.UTF_8).newDecoder(); final CharBuffer result = CharBuffer.allocate(content.length()); + ByteBuffer bb = null; + CharsetDecoder charsetDecoder = null; while (cb.hasRemaining()) { final char c = cb.get(); if (c == '%' && cb.remaining() >= 2) { @@ -230,6 +245,12 @@ static String decode(final CharSequence content, final Charset charset, final bo final int u = Character.digit(uc, RADIX); final int l = Character.digit(lc, RADIX); if (u != -1 && l != -1) { + if (bb == null) { + bb = ByteBuffer.allocate(cb.remaining() + 1); + } + if (charsetDecoder == null) { + charsetDecoder = (charset != null ? charset : StandardCharsets.UTF_8).newDecoder(); + } bb.put((byte) ((u << 4) + l)); } else { flushBin(bb, result, charsetDecoder);