diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/FormEncodingBuilderTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/FormEncodingBuilderTest.java index a9533bf78..8c2eca817 100644 --- a/okhttp-tests/src/test/java/com/squareup/okhttp/FormEncodingBuilderTest.java +++ b/okhttp-tests/src/test/java/com/squareup/okhttp/FormEncodingBuilderTest.java @@ -25,11 +25,12 @@ public final class FormEncodingBuilderTest { RequestBody formEncoding = new FormEncodingBuilder() .add("a&b", "c=d") .add("space, the", "final frontier") + .add("%25", "%25") .build(); assertEquals("application/x-www-form-urlencoded", formEncoding.contentType().toString()); - String expected = "a%26b=c%3Dd&space%2C+the=final+frontier"; + String expected = "a%26b=c%3Dd&space,%20the=final%20frontier&%2525=%2525"; assertEquals(expected.length(), formEncoding.contentLength()); Buffer out = new Buffer(); @@ -37,6 +38,19 @@ public final class FormEncodingBuilderTest { assertEquals(expected, out.readUtf8()); } + @Test public void addEncoded() throws Exception { + RequestBody formEncoding = new FormEncodingBuilder() + .addEncoded("a+=& b", "c+=& d") + .addEncoded("e+=& f", "g+=& h") + .addEncoded("%25", "%25") + .build(); + + String expected = "a%20%3D%26%20b=c%20%3D%26%20d&e%20%3D%26%20f=g%20%3D%26%20h&%25=%25"; + Buffer out = new Buffer(); + formEncoding.writeTo(out); + assertEquals(expected, out.readUtf8()); + } + @Test public void encodedPair() throws Exception { RequestBody formEncoding = new FormEncodingBuilder() .add("sim", "ple") diff --git a/okhttp/src/main/java/com/squareup/okhttp/FormEncodingBuilder.java b/okhttp/src/main/java/com/squareup/okhttp/FormEncodingBuilder.java index 63eac1ab9..8a253dafc 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/FormEncodingBuilder.java +++ b/okhttp/src/main/java/com/squareup/okhttp/FormEncodingBuilder.java @@ -15,8 +15,6 @@ */ package com.squareup.okhttp; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; import okio.Buffer; /** @@ -34,13 +32,24 @@ public final class FormEncodingBuilder { if (content.size() > 0) { content.writeByte('&'); } - try { - content.writeUtf8(URLEncoder.encode(name, "UTF-8")); - content.writeByte('='); - content.writeUtf8(URLEncoder.encode(value, "UTF-8")); - } catch (UnsupportedEncodingException e) { - throw new AssertionError(e); + HttpUrl.canonicalize(content, name, 0, name.length(), + HttpUrl.QUERY_COMPONENT_ENCODE_SET, false, true); + content.writeByte('='); + HttpUrl.canonicalize(content, value, 0, value.length(), + HttpUrl.QUERY_COMPONENT_ENCODE_SET, false, true); + return this; + } + + /** Add new key-value pair. */ + public FormEncodingBuilder addEncoded(String name, String value) { + if (content.size() > 0) { + content.writeByte('&'); } + HttpUrl.canonicalize(content, name, 0, name.length(), + HttpUrl.QUERY_COMPONENT_ENCODE_SET, true, true); + content.writeByte('='); + HttpUrl.canonicalize(content, value, 0, value.length(), + HttpUrl.QUERY_COMPONENT_ENCODE_SET, true, true); return this; } diff --git a/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java b/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java index 2cda2c47f..709372d69 100644 --- a/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java +++ b/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java @@ -255,12 +255,12 @@ import okio.Buffer; public final class HttpUrl { private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; - private static final String USERNAME_ENCODE_SET = " \"':;<=>@[]^`{}|/\\?#"; - private static final String PASSWORD_ENCODE_SET = " \"':;<=>@[]\\^`{}|/\\?#"; - private static final String PATH_SEGMENT_ENCODE_SET = " \"<>^`{}|/\\?#"; - private static final String QUERY_ENCODE_SET = " \"'<>#"; - private static final String QUERY_COMPONENT_ENCODE_SET = " \"'<>#&="; - private static final String FRAGMENT_ENCODE_SET = ""; + static final String USERNAME_ENCODE_SET = " \"':;<=>@[]^`{}|/\\?#"; + static final String PASSWORD_ENCODE_SET = " \"':;<=>@[]\\^`{}|/\\?#"; + static final String PATH_SEGMENT_ENCODE_SET = " \"<>^`{}|/\\?#"; + static final String QUERY_ENCODE_SET = " \"'<>#"; + static final String QUERY_COMPONENT_ENCODE_SET = " \"'<>#&="; + static final String FRAGMENT_ENCODE_SET = ""; /** Either "http" or "https". */ private final String scheme; @@ -1422,10 +1422,10 @@ public final class HttpUrl { || (codePoint == '%' && !alreadyEncoded) || (query && codePoint == '+')) { // Slow path: the character at i requires encoding! - StringBuilder out = new StringBuilder(); - out.append(input, pos, i); + Buffer out = new Buffer(); + out.writeUtf8(input, pos, i); canonicalize(out, input, i, limit, encodeSet, alreadyEncoded, query); - return out.toString(); + return out.readUtf8(); } } @@ -1433,7 +1433,7 @@ public final class HttpUrl { return input.substring(pos, limit); } - static void canonicalize(StringBuilder out, String input, int pos, int limit, + static void canonicalize(Buffer out, String input, int pos, int limit, String encodeSet, boolean alreadyEncoded, boolean query) { Buffer utf8Buffer = null; // Lazily allocated. int codePoint; @@ -1444,7 +1444,7 @@ public final class HttpUrl { // Skip this character. } else if (query && codePoint == '+') { // HTML permits space to be encoded as '+'. We use '%20' to avoid special cases. - out.append(alreadyEncoded ? "%20" : "%2B"); + out.writeUtf8(alreadyEncoded ? "%20" : "%2B"); } else if (codePoint < 0x20 || codePoint >= 0x7f || encodeSet.indexOf(codePoint) != -1 @@ -1456,13 +1456,13 @@ public final class HttpUrl { utf8Buffer.writeUtf8CodePoint(codePoint); while (!utf8Buffer.exhausted()) { int b = utf8Buffer.readByte() & 0xff; - out.append('%'); - out.append(HEX_DIGITS[(b >> 4) & 0xf]); - out.append(HEX_DIGITS[b & 0xf]); + out.writeByte('%'); + out.writeByte(HEX_DIGITS[(b >> 4) & 0xf]); + out.writeByte(HEX_DIGITS[b & 0xf]); } } else { // This character doesn't need encoding. Just copy it over. - out.append((char) codePoint); + out.writeUtf8CodePoint(codePoint); } } }