diff --git a/core/src/main/scala/sttp/model/internal/Rfc3986.scala b/core/src/main/scala/sttp/model/internal/Rfc3986.scala index 3afb9ce5..5cb08a6f 100644 --- a/core/src/main/scala/sttp/model/internal/Rfc3986.scala +++ b/core/src/main/scala/sttp/model/internal/Rfc3986.scala @@ -15,17 +15,47 @@ object Rfc3986 { val QueryWithBrackets: Set[Char] = Query ++ Set('[', ']') - /** @param spaceAsPlus + /** Encode string using UTF-8 + * @param spaceAsPlus * In the query, space is encoded as a `+`. In other contexts, it should be %-encoded as `%20`. * @param encodePlus * Should `+` (which is the encoded form of space in the query) be %-encoded. */ def encode(allowedCharacters: Set[Char], spaceAsPlus: Boolean = false, encodePlus: Boolean = false)( s: String + ): String = + encode(s, "UTF-8", allowedCharacters, spaceAsPlus, encodePlus) + + + /** Encode string using encoding, leave allowedCharacters as they are + * @param s + * String to be encoded + * @param enc + * Encoding to be used + * @param allowedCharacters + * Characters to be not to be encoded + */ + def encode(s: String, enc: String, allowedCharacters: Set[Char]): String = + encode(s, enc, allowedCharacters, spaceAsPlus = false, encodePlus = false) + + /** @param spaceAsPlus + * In the query, space is encoded as a `+`. In other contexts, it should be %-encoded as `%20`. + * @param encodePlus + * Should `+` (which is the encoded form of space in the query) be %-encoded. + */ + private def encode( + s: String, + enc: String, + allowedCharacters: Set[Char], + spaceAsPlus: Boolean, + encodePlus: Boolean ): String = { val sb = new StringBuilder() // based on https://gist.github.com/teigen/5865923 - for (b <- s.getBytes("UTF-8")) { + val bytes: Array[Byte] = s.getBytes(enc) + var i = 0 + while (i < bytes.length) { + val b: Byte = bytes(i) val c = (b & 0xff).toChar if (c == '+' && encodePlus) sb.append("%2B") // #48 else if (allowedCharacters(c)) sb.append(c) @@ -34,6 +64,7 @@ object Rfc3986 { sb.append("%") sb.append(Rfc3986Compatibility.formatByte(b)) } + i += 1 } sb.toString } diff --git a/core/src/main/scalajs/sttp/model/internal/UriCompatibility.scala b/core/src/main/scalajs/sttp/model/internal/UriCompatibility.scala index e3fb4167..87c13a1e 100644 --- a/core/src/main/scalajs/sttp/model/internal/UriCompatibility.scala +++ b/core/src/main/scalajs/sttp/model/internal/UriCompatibility.scala @@ -11,4 +11,6 @@ private[sttp] object UriCompatibility { } def encodeQuery(s: String, enc: String): String = URIUtils.encodeURIComponent(s) + + def encodeBodyPart(s: String, enc: String): String = URIUtils.encodeURIComponent(s) } diff --git a/core/src/main/scalajvm/sttp/model/internal/UriCompatibility.scala b/core/src/main/scalajvm/sttp/model/internal/UriCompatibility.scala index 0adda880..62059559 100644 --- a/core/src/main/scalajvm/sttp/model/internal/UriCompatibility.scala +++ b/core/src/main/scalajvm/sttp/model/internal/UriCompatibility.scala @@ -19,4 +19,6 @@ private[sttp] object UriCompatibility { } def encodeQuery(s: String, enc: String): String = URLEncoder.encode(s, enc) + + def encodeBodyPart(s: String, enc: String): String = Rfc3986.encode(s, enc, Rfc3986.Unreserved) } diff --git a/core/src/main/scalanative/sttp/model/internal/UriCompatibility.scala b/core/src/main/scalanative/sttp/model/internal/UriCompatibility.scala index 057ba56d..e3291d2b 100644 --- a/core/src/main/scalanative/sttp/model/internal/UriCompatibility.scala +++ b/core/src/main/scalanative/sttp/model/internal/UriCompatibility.scala @@ -21,4 +21,6 @@ private[sttp] object UriCompatibility { } def encodeQuery(s: String, enc: String): String = URLEncoder.encode(s, enc) + + def encodeBodyPart(s: String, enc: String): String = Rfc3986.encode(s, enc, Rfc3986.Unreserved) } diff --git a/core/src/test/scala/sttp/model/UriTests.scala b/core/src/test/scala/sttp/model/UriTests.scala index 0c08e3fe..24bc91f7 100644 --- a/core/src/test/scala/sttp/model/UriTests.scala +++ b/core/src/test/scala/sttp/model/UriTests.scala @@ -1,13 +1,14 @@ package sttp.model -import java.net.URI - -import Uri._ import org.scalatest.TryValues import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers +import sttp.model.Uri._ +import sttp.model.internal.UriCompatibility + +import java.net.URI -class UriTests extends AnyFunSuite with Matchers with TryValues { +class UriTests extends AnyFunSuite with Matchers with TryValues with UriTestsExtension { val HS = HostSegment val PS = PathSegment @@ -116,6 +117,9 @@ class UriTests extends AnyFunSuite with Matchers with TryValues { List(QS.KeyValue("k1&", "v1&", valueEncoding = QuerySegmentEncoding.Relaxed)) -> "k1%26=v1&", List(QS.Plain("ą/ę&+;?", encoding = QuerySegmentEncoding.Relaxed)) -> "%C4%85/%C4%99&+;?", List(QS.KeyValue("k", "v1,v2", valueEncoding = QuerySegmentEncoding.All)) -> "k=v1%2Cv2", + List(QS.KeyValue("k", "v1-v2", valueEncoding = QuerySegmentEncoding.All)) -> "k=v1-v2", + List(QS.KeyValue("k", "v1_v2", valueEncoding = QuerySegmentEncoding.All)) -> "k=v1_v2", + List(QS.KeyValue("k", "v1.v2", valueEncoding = QuerySegmentEncoding.All)) -> "k=v1.v2", List(QS.KeyValue("k", "v1,v2")) -> "k=v1,v2", List(QS.KeyValue("k", "+1234")) -> "k=%2B1234", List(QS.KeyValue("k", "[]")) -> "k=%5B%5D", @@ -130,6 +134,22 @@ class UriTests extends AnyFunSuite with Matchers with TryValues { } } + private val bodyPartEncodingTestData = List( + "v1,v2" -> "v1%2Cv2", + "v1-v2" -> "v1-v2", + "v1~v2" -> "v1~v2", + "v1_v2" -> "v1_v2", + "v1.v2" -> "v1.v2", + ) + + for { + (segments, expected) <- bodyPartEncodingTestData + } { + test(s"$segments should serialize to$expected") { + UriCompatibility.encodeBodyPart(segments, "utf-8") should endWith(expected) + } + } + val hostTestData = List( "www.mikołak.net" -> "http://www.xn--mikoak-6db.net", "192.168.1.0" -> "http://192.168.1.0", diff --git a/core/src/test/scalajs/sttp/model/UriTestsExtension.scala b/core/src/test/scalajs/sttp/model/UriTestsExtension.scala new file mode 100644 index 00000000..7d630ad3 --- /dev/null +++ b/core/src/test/scalajs/sttp/model/UriTestsExtension.scala @@ -0,0 +1,21 @@ +package sttp.model + +import org.scalatest.TryValues +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers +import sttp.model.Uri._ + +trait UriTestsExtension extends AnyFunSuite with Matchers with TryValues { this: UriTests => + + private val tildeEncodingTest = List( + List(QS.KeyValue("k", "v1~v2", valueEncoding = QuerySegmentEncoding.All)) -> "k=v1~v2" + ) + + for { + (segments, expected) <- tildeEncodingTest + } { + test(s"$segments should serialize to$expected") { + testUri.copy(querySegments = segments).toString should endWith(expected) + } + } +} diff --git a/core/src/test/scalajvm/sttp/model/UriTestsExtension.scala b/core/src/test/scalajvm/sttp/model/UriTestsExtension.scala new file mode 100644 index 00000000..ef5e5685 --- /dev/null +++ b/core/src/test/scalajvm/sttp/model/UriTestsExtension.scala @@ -0,0 +1,21 @@ +package sttp.model + +import org.scalatest.TryValues +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers +import sttp.model.Uri._ + +trait UriTestsExtension extends AnyFunSuite with Matchers with TryValues { this: UriTests => + + private val tildeEncodingTest = List( + List(QS.KeyValue("k", "v1~v2", valueEncoding = QuerySegmentEncoding.All)) -> "k=v1%7Ev2" + ) + + for { + (segments, expected) <- tildeEncodingTest + } { + test(s"$segments should serialize to$expected") { + testUri.copy(querySegments = segments).toString should endWith(expected) + } + } +} diff --git a/core/src/test/scalanative/sttp/model/UriTestsExtension.scala b/core/src/test/scalanative/sttp/model/UriTestsExtension.scala new file mode 100644 index 00000000..5b8c6090 --- /dev/null +++ b/core/src/test/scalanative/sttp/model/UriTestsExtension.scala @@ -0,0 +1,8 @@ +package sttp.model + +import org.scalatest.TryValues +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers +import sttp.model.Uri._ + +trait UriTestsExtension \ No newline at end of file