Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions http/src/main/scala/spinoco/protocol/http/Uri.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import scodec.bits.BitVector
import spinoco.protocol.common.util._
import spinoco.protocol.common.codec._
import spinoco.protocol.common.Terminator
import spinoco.protocol.http.Uri.QueryParameter.Multi
import spinoco.protocol.http.codec.RFC3986

import scala.annotation.tailrec

Expand Down Expand Up @@ -116,7 +116,7 @@ object Uri {
def stringify:String = {
val sb = new StringBuilder()
if (self.initialSlash) sb.append("/")
sb.append(self.segments.map(s => URLEncoder.encode(s, "UTF-8")).mkString("/"))
sb.append(self.segments.map(RFC3986.encodePathSegment).mkString("/"))
if (self.trailingSlash) sb.append("/")
sb.toString()
}
Expand All @@ -125,6 +125,8 @@ object Uri {

object Path {

private val PlusRegex = "\\+".r

/** constructs relative path without initial slash (`/`) **/
def relative(s: String) : Path =
Path(initialSlash = false, trailingSlash = false, segments = Seq(s))
Expand All @@ -137,7 +139,11 @@ object Uri {

def fromUtf8String(path: String):Uri.Path = {
val trimmed = path.trim
val segments = trimmed.split("/").filter(_.nonEmpty).map(s => URLDecoder.decode(s, "UTF-8"))
val segments = trimmed.split("/").filter(_.nonEmpty).map { s =>
// avoid URLDecoder turning a + into a space
val segment = PlusRegex.replaceAllIn(s, "%2B")
URLDecoder.decode(segment, "UTF-8")
}
Path(
initialSlash = trimmed.startsWith("/")
, segments = segments
Expand Down
51 changes: 51 additions & 0 deletions http/src/main/scala/spinoco/protocol/http/codec/RFC3986.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package spinoco.protocol.http.codec

import java.nio.charset.StandardCharsets

import scala.collection.immutable.BitSet

/**
* https://tools.ietf.org/html/rfc3986
*/
object RFC3986 {

val genDelims = BitSet(':', '/', '?', '#', '[', ']', '@')

val subDelims = BitSet('!', '$', '&', ''', '(', ')' , '*', '+', ',', ';', '=')

val reserved = genDelims ++ subDelims

val alpha = BitSet((('a' to 'z') ++ ('A' to 'Z')).map(_.toInt): _*)

val digit = BitSet(('0' to '9').map(_.toInt): _*)

val unreserved = alpha ++ digit ++ BitSet('-', '.', '_', '~')

val pchar = unreserved ++ subDelims ++ BitSet(':', '@')


// https://tools.ietf.org/html/rfc3986#section-3.1
val scheme = alpha ++ digit ++ BitSet('+', '-', '.')

// https://tools.ietf.org/html/rfc3986#section-3.3
val pathSegment = pchar


def encode(str: String, allowedChars: BitSet): String = {
// add a buffer to hopefully account for all chars that need to be escaped
val sb = new StringBuilder(str.length * 12 / 10)
str.foreach { c =>
if (allowedChars.contains(c)) sb.append(c)
else {
// https://tools.ietf.org/html/rfc3986#section-2.5
c.toString.getBytes(StandardCharsets.UTF_8).foreach { b =>
sb.append("%" + "%02X".format(b))
}
}
}
sb.mkString
}

def encodePathSegment(segment: String): String = encode(segment, pathSegment)

}
4 changes: 4 additions & 0 deletions http/src/test/scala/spinoco/protocol/http/UriSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ object UriSpec extends Properties("Uri") {
, Uri(HttpScheme.HTTP, HostPort("www.spinoco.com", None), Uri.Path.Root, Uri.Query.empty)
, "http://www.spinoco.com/"
)
, ("http://www.spinoco.com/aA0-._~/!$&'()*+,;=/:@/%5B%5D%2F%7B%7D%C3%A9"
, Uri(HttpScheme.HTTP, HostPort("www.spinoco.com", None), Uri.Path.Root / "aA0-._~" / "!$&'()*+,;=" / ":@" / "[]/{}é", Uri.Query.empty)
, "http://www.spinoco.com/aA0-._~/!$&'()*+,;=/:@/%5B%5D%2F%7B%7D%C3%A9"
)
, ("http://x.com/123?a=1&b=2;c=3"
, Uri(HttpScheme.HTTP, HostPort("x.com", None), Uri.Path.Root / "123", Uri.Query("a", "1") :+ (QueryParameter.single("b", "2") :+ ("c", "3")))
, "http://x.com/123?a=1&b=2;c=3"
Expand Down