|
|
// Completely copied from org.http4s.dsl.impl
package outwatch.router
import cats.implicits._ import java.nio.{ByteBuffer, CharBuffer} import java.nio.charset.Charset import java.nio.charset.StandardCharsets.UTF_8
/** Base class for path extractors. */ trait Path { def /(child: String) = new /(this, child) def toList: List[String] def parent: Path def lastOption: Option[String] def startsWith(other: Path): Boolean def pathString: String def toUrlString: String = if (pathString.isEmpty) "/" else pathString }
object Path {
/** Constructs a path from a single string by splitting on the `'/'`
* character. * * Leading slashes do not create an empty path segment. This is to * reflect that there is no distinction between a request to * `http://www.example.com` from `http://www.example.com/`. * * Trailing slashes result in a path with an empty final segment, * unless the path is `"/"`, which is `Root`. * * Segments are URL decoded. * * {{{ * scala> Path("").toList * res0: List[String] = List() * scala> Path("/").toList * res1: List[String] = List() * scala> Path("a").toList * res2: List[String] = List(a) * scala> Path("/a").toList * res3: List[String] = List(a) * scala> Path("/a/").toList * res4: List[String] = List(a, "") * scala> Path("//a").toList * res5: List[String] = List("", a) * scala> Path("/%2F").toList * res0: List[String] = List(/) * }}} */ def apply(str: String): Path = if (str == "" || str == "/") Root else { val segments = str.split("/", -1) // .head is safe because split always returns non-empty array
val segments0 = if (segments.head == "") segments.drop(1) else segments segments0.foldLeft(Root: Path)((path, seg) => path / UrlCodingUtils.urlDecode(seg)) }
def apply(first: String, rest: String*): Path = rest.foldLeft(Root / first)(_ / _)
def apply(list: List[String]): Path = list.foldLeft(Root: Path)(_ / _)
def apply(left: Path, right: Path): Path = right.toList.foldLeft(left)(_ / _)
def unapplySeq(path: Path): Some[List[String]] = Some(path.toList) //
// def unapplySeq[F[_]](request: Request[F]): Some[List[String]] =
// Some(Path(request.pathInfo).toList)
}
final case class /(parent: Path, child: String) extends Path { lazy val toList: List[String] = parent.toList ++ List(child)
def lastOption: Some[String] = Some(child)
lazy val asString: String = s"${parent.pathString}/${UrlCodingUtils.pathEncode(child)}"
override def toString: String = asString
def pathString: String = asString
def startsWith(other: Path): Boolean = { val components = other.toList toList.take(components.length) === components } }
/**
* Path separator extractor: * {{{ * Path("/1/2/3/test.json") match { * case "1" /: "2" /: _ => ... * }}} */ object /: { def unapply(path: Path): Option[(String, Path)] = path.toList match { case head :: tail => Some(head -> Path(tail)) case Nil => None } }
/**
* Root extractor: * {{{ * Path("/") match { * case Root => ... * } * }}} */ case object Root extends Path { def toList: List[String] = Nil def parent: Path = this def lastOption: None.type = None override def toString = "Root" def pathString: String = "" def startsWith(other: Path): Boolean = other == Root }
private[router] object UrlCodingUtils {
private val lower = ('a' to 'z').toSet private val upper = ('A' to 'Z').toSet private val num = ('0' to '9').toSet val Unreserved: Set[Char] = lower ++ upper ++ num ++ "-_.~"
private val toSkip : Set[Char] = Unreserved ++ "!$&'()*+,;=:/?@"
private val HexUpperCaseChars: Array[Char] = ('A' to 'F').toArray /**
* Percent-encodes a string. Depending on the parameters, this method is * appropriate for URI or URL form encoding. Any resulting percent-encodings * are normalized to uppercase. * * @param toEncode the string to encode * @param charset the charset to use for characters that are percent encoded * @param spaceIsPlus if space is not skipped, determines whether it will be * rendreed as a `"+"` or a percent-encoding according to `charset`. * @param toSkip a predicate of characters exempt from encoding. In typical * use, this is composed of all Unreserved URI characters and sometimes a * subset of Reserved URI characters. */ def urlEncode( toEncode: String, charset: Charset = UTF_8, spaceIsPlus: Boolean = false, toSkip: Char => Boolean = toSkip): String = { val in = charset.encode(toEncode) val out = CharBuffer.allocate((in.remaining() * 3).toInt) while (in.hasRemaining) { val c = in.get().toChar if (toSkip(c)) { out.put(c) } else if (c == ' ' && spaceIsPlus) { out.put('+') } else { out.put('%') out.put(HexUpperCaseChars((c >> 4) & 0xF)) out.put(HexUpperCaseChars(c & 0xF)) } } out.flip() out.toString }
private val SkipEncodeInPath = Unreserved ++ ":@!$&'()*+,;="
def pathEncode(s: String, charset: Charset = UTF_8): String = UrlCodingUtils.urlEncode(s, charset, false, SkipEncodeInPath)
/**
* Percent-decodes a string. * * @param toDecode the string to decode * @param charset the charset of percent-encoded characters * @param plusIsSpace true if `'+'` is to be interpreted as a `' '` * @param toSkip a predicate of characters whose percent-encoded form * is left percent-encoded. Almost certainly should be left empty. */ def urlDecode( toDecode: String, charset: Charset = UTF_8, plusIsSpace: Boolean = false, toSkip: Char => Boolean = Function.const(false)): String = { val in = CharBuffer.wrap(toDecode) // reserve enough space for 3-byte UTF-8 characters. 4-byte characters are represented
// as surrogate pairs of characters, and will get a luxurious 6 bytes of space.
val out = ByteBuffer.allocate(in.remaining() * 3) while (in.hasRemaining) { val mark = in.position() val c = in.get() if (c == '%') { if (in.remaining() >= 2) { val xc = in.get() val yc = in.get() // scalastyle:off magic.number
val x = Character.digit(xc, 0x10) val y = Character.digit(yc, 0x10) // scalastyle:on magic.number
if (x != -1 && y != -1) { val oo = (x << 4) + y if (!toSkip(oo.toChar)) { out.put(oo.toByte) } else { out.put('%'.toByte) out.put(xc.toByte) out.put(yc.toByte) } } else { out.put('%'.toByte) in.position(mark + 1) } } else { // This is an invalid encoding. Fail gracefully by treating the '%' as
// a literal.
out.put(c.toByte) while (in.hasRemaining) out.put(in.get().toByte) } } else if (c == '+' && plusIsSpace) { out.put(' '.toByte) } else { // normally `out.put(c.toByte)` would be enough since the url is %-encoded,
// however there are cases where a string can be partially decoded
// so we have to make sure the non us-ascii chars get preserved properly.
if (this.toSkip(c)) { out.put(c.toByte) } else { out.put(charset.encode(String.valueOf(c))) } } } out.flip() charset.decode(out).toString } }
|