diff options
Diffstat (limited to 'javalib/src/main/scala/java/net/URI.scala')
-rw-r--r-- | javalib/src/main/scala/java/net/URI.scala | 706 |
1 files changed, 706 insertions, 0 deletions
diff --git a/javalib/src/main/scala/java/net/URI.scala b/javalib/src/main/scala/java/net/URI.scala new file mode 100644 index 0000000..c969f55 --- /dev/null +++ b/javalib/src/main/scala/java/net/URI.scala @@ -0,0 +1,706 @@ +package java.net + +import scala.scalajs.js.RegExp +import scala.scalajs.js +import scala.scalajs.js.{decodeURIComponent => decode} + +import scala.annotation.tailrec + +final class URI(origStr: String) extends Serializable with Comparable[URI] { + + import URI.Fields._ + + /** The fields matched in the regular expression. + * + * This is a local val for the primary constructor. It is a val, + * since we'll set it to null after initializing all fields. + */ + private[this] var _fld = Option(URI.uriRe.exec(origStr)).getOrElse { + throw new URISyntaxException(origStr, "Malformed URI") + } + + private val _isAbsolute = fld(AbsScheme).isDefined + private val _isOpaque = fld(AbsOpaquePart).isDefined + + @inline private def fld(idx: Int): js.UndefOr[String] = _fld(idx) + + @inline private def fld(absIdx: Int, relIdx: Int): js.UndefOr[String] = + if (_isAbsolute) _fld(absIdx) else _fld(relIdx) + + private val _scheme = fld(AbsScheme) + + private val _schemeSpecificPart = { + if (!_isAbsolute) fld(RelSchemeSpecificPart) + else if (_isOpaque) fld(AbsOpaquePart) + else fld(AbsHierPart) + }.get + + private val _authority = fld(AbsAuthority, RelAuthority) + private val _userInfo = fld(AbsUserInfo, RelUserInfo) + private val _host = fld(AbsHost, RelHost) + private val _port = fld(AbsPort, RelPort).fold(-1)(_.toInt) + + private val _path = { + if (_isAbsolute) { + if (_authority.isDefined) fld(AbsNetPath) + else fld(AbsAbsPath) + } else { + if (_authority.isDefined) fld(RelNetPath) + else fld(RelAbsPath) orElse fld(RelRelPath) + } + } + + private val _query = fld(AbsQuery, RelQuery) + private val _fragment = fld(Fragment) + + // End of default ctor. Unset helper field + _fld = null + + def this(scheme: String, ssp: String, fragment: String) = + this(URI.uriStr(scheme, ssp, fragment)) + + def this(scheme: String, userInfo: String, host: String, port: Int, + path: String, query: String, fragment: String) = { + this(URI.uriStr(scheme, userInfo, host, port, path, query, fragment)) + parseServerAuthority() + } + + def this(scheme: String, host: String, path: String, fragment: String) = + this(scheme, null, host, -1, path, null, fragment) + + def this(scheme: String, authority: String, path: String, query: String, + fragment: String) = { + this(URI.uriStr(scheme, authority, path, query, fragment)) + // JavaDoc says to invoke parseServerAuthority() here, but in practice + // it isn't invoked. This makes sense, since you want to be able + // to create URIs with registry-based authorities. + // parseServerAuthority() + } + + /** Compare this URI to another URI while supplying a comparator + * + * This helper is required to account for the semantic differences + * between [[compareTo]] and [[equals]]. ([[equals]] does treat + * URI escapes specially: they are never case-sensitive). + */ + @inline + private def internalCompare(that: URI)(cmp: (String, String) => Int): Int = { + @inline def cmpOpt(x: js.UndefOr[String], y: js.UndefOr[String]): Int = { + if (x == y) 0 + // Undefined components are considered less than defined components + else x.fold(-1)(s1 => y.fold(1)(s2 => cmp(s1, s2))) + } + + if (this._scheme != that._scheme) + this._scheme.fold(-1)(s1 => that._scheme.fold(1)(s1.compareToIgnoreCase)) + else if (this._isOpaque != that._isOpaque) + // A hierarchical URI is less than an opaque URI + if (this._isOpaque) 1 else -1 + else if (_isOpaque) { + val ssp = cmp(this._schemeSpecificPart, that._schemeSpecificPart) + if (ssp != 0) ssp + else cmpOpt(this._fragment, that._fragment) + } else if (this._authority != that._authority) { + if (this._host.isDefined && that._host.isDefined) { + val ui = cmpOpt(this._userInfo, that._userInfo) + if (ui != 0) ui + else { + val hst = this._host.get.compareToIgnoreCase(that._host.get) + if (hst != 0) hst + else if (this._port == that._port) 0 + else if (this._port == -1) -1 + else if (that._port == -1) 1 + else this._port - that._port + } + } else + cmpOpt(this._authority, that._authority) + } else if (this._path != that._path) + cmpOpt(this._path, that._path) + else if (this._query != that._query) + cmpOpt(this._query, that._query) + else + cmpOpt(this._fragment, that._fragment) + } + + def compareTo(that: URI): Int = internalCompare(that)(_.compareTo(_)) + + override def equals(that: Any): Boolean = that match { + case that: URI => internalCompare(that)(URI.escapeAwareCompare) == 0 + case _ => false + } + + def getAuthority(): String = _authority.map(decode).orNull + def getFragment(): String = _fragment.map(decode).orNull + def getHost(): String = _host.orNull + def getPath(): String = _path.map(decode).orNull + def getPort(): Int = _port + def getQuery(): String = _query.map(decode).orNull + def getRawAuthority(): String = _authority.orNull + def getRawFragment(): String = _fragment.orNull + def getRawPath(): String = _path.orNull + def getRawQuery(): String = _query.orNull + def getRawSchemeSpecificPart(): String = _schemeSpecificPart + def getRawUserInfo(): String = _userInfo.orNull + def getScheme(): String = _scheme.orNull + def getSchemeSpecificPart(): String = decode(_schemeSpecificPart) + def getUserInfo(): String = _userInfo.map(decode).orNull + + override def hashCode(): Int = { + import scala.util.hashing.MurmurHash3._ + import URI.normalizeEscapes + + var acc = URI.uriSeed + acc = mix(acc, _scheme.##) // scheme may not contain escapes + acc = mix(acc, normalizeEscapes(_schemeSpecificPart).##) + acc = mixLast(acc, _fragment.map(normalizeEscapes).##) + + finalizeHash(acc, 3) + } + + def isAbsolute(): Boolean = _isAbsolute + def isOpaque(): Boolean = _isOpaque + + def normalize(): URI = if (_isOpaque || _path.isEmpty) this else { + val origPath = _path.get + + // Step 1: Remove all "." segments + // Step 2: Remove ".." segments preceeded by non ".." segment until no + // longer applicable + + /** Checks whether a successive ".." may drop the head of a + * reversed segment list. + */ + def okToDropFrom(resRev: List[String]) = + resRev.nonEmpty && resRev.head != ".." && resRev.head != "" + + @tailrec + def loop(in: List[String], resRev: List[String]): List[String] = in match { + case "." :: Nil => + // convert "." segments at end to an empty segment + // (consider: /a/b/. => /a/b/, not /a/b) + loop(Nil, "" :: resRev) + case ".." :: Nil if okToDropFrom(resRev) => + // prevent a ".." segment at end to change a "dir" into a "file" + // (consider: /a/b/.. => /a/, not /a) + loop(Nil, "" :: resRev.tail) + case "." :: xs => + // remove "." segments + loop(xs, resRev) + case "" :: xs if xs.nonEmpty => + // remove empty segments not at end of path + loop(xs, resRev) + case ".." :: xs if okToDropFrom(resRev) => + // Remove preceeding non-".." segment + loop(xs, resRev.tail) + case x :: xs => + loop(xs, x :: resRev) + case Nil => + resRev.reverse + } + + // Split into segments. -1 since we want empty trailing ones + val segments0 = origPath.split("/", -1).toList + val isAbsPath = segments0.nonEmpty && segments0.head == "" + // Don't inject first empty segment into normalization loop, so we + // won't need to special case it. + val segments1 = if (isAbsPath) segments0.tail else segments0 + val segments2 = loop(segments1, Nil) + + // Step 3: If path is relative and first segment contains ":", prepend "." + // segment (according to JavaDoc). If it is absolute, add empty + // segment again to have leading "/". + val segments3 = { + if (isAbsPath) + "" :: segments2 + else if (segments2.nonEmpty && segments2.head.contains(':')) + "." :: segments2 + else segments2 + } + + val newPath = segments3.mkString("/") + + // Only create new instance if anything changed + if (newPath == origPath) + this + else + new URI(getScheme(), getRawAuthority(), newPath, getQuery(), getFragment()) + } + + def parseServerAuthority(): URI = { + if (_authority.nonEmpty && _host.isEmpty) + throw new URISyntaxException(origStr, "No Host in URI") + else this + } + + def relativize(uri: URI): URI = { + def authoritiesEqual = this._authority.fold(uri._authority.isEmpty) { a1 => + uri._authority.fold(false)(a2 => URI.escapeAwareCompare(a1, a2) == 0) + } + + if (this.isOpaque || uri.isOpaque || + this._scheme != uri._scheme || !authoritiesEqual) uri + else { + val thisN = this.normalize() + val uriN = uri.normalize() + + // Strangely, Java doesn't handle escapes here. So we don't + if (uriN.getRawPath().startsWith(thisN.getRawPath())) { + val newPath = uriN.getRawPath().stripPrefix(thisN.getRawPath()) + + new URI(scheme = null, authority = null, + // never produce an abs path if we relativized + path = newPath.stripPrefix("/"), + query = uri.getQuery(), fragment = uri.getFragment()) + } else uri + } + } + + def resolve(str: String): URI = resolve(URI.create(str)) + + def resolve(uri: URI): URI = { + if (uri.isAbsolute() || this.isOpaque()) uri + else if (uri._scheme.isEmpty && uri._authority.isEmpty && + uri._path.get == "" && uri._query.isEmpty) + // This is a special case for URIs like: "#foo". This allows to + // just change the fragment in the current document. + new URI( + this.getScheme(), + this.getRawAuthority(), + this.getRawPath(), + this.getRawQuery(), + uri.getRawFragment()) + else if (uri._authority.isDefined) + new URI( + this.getScheme(), + uri.getRawAuthority(), + uri.getRawPath(), + uri.getRawQuery(), + uri.getRawFragment()) + else if (uri._path.get.startsWith("/")) + new URI( + this.getScheme(), + this.getRawAuthority(), + uri.getRawPath(), + uri.getRawQuery(), + uri.getRawFragment()) + else { + val basePath = this._path.get + val relPath = uri._path.get + val endIdx = basePath.lastIndexOf('/') + val path = + if (endIdx == -1) relPath + else basePath.substring(0, endIdx+1) + relPath + new URI( + this.getScheme(), + this.getAuthority(), + path, + uri.getRawQuery(), + uri.getRawFragment()).normalize() + } + } + + def toASCIIString(): String = origStr // We allow only ASCII in URIs. + override def toString(): String = origStr + + // Not implemented: + // def toURL(): URL + +} + +object URI { + + def create(str: String): URI = { + try new URI(str) + catch { + case e: URISyntaxException => throw new IllegalArgumentException(e) + } + } + + // IPv4address = 1*digit "." 1*digit "." 1*digit "." 1*digit + private final val ipv4address = "[0-9]{1,3}(?:\\.[0-9]{1,3}){3}" + + private final val ipv6address = { + // http://stackoverflow.com/a/17871737/1149944 + val block = "[0-9a-f]{1,4}" + val lelem = "(?:"+block+":)" + val relem = "(?::"+block+")" + val ipv4 = ipv4address + + "(?:" + + lelem+"{7}"+block+"|"+ // 1:2:3:4:5:6:7:8 + lelem+"{1,7}:|"+ // 1:: 1:2:3:4:5:6:7:: + lelem+"{1,6}"+relem+"|"+ // 1::8 1:2:3:4:5:6::8 1:2:3:4:5:6::8 + lelem+"{1,5}"+relem+"{1,2}|"+ // 1::7:8 1:2:3:4:5::7:8 1:2:3:4:5::8 + lelem+"{1,4}"+relem+"{1,3}|"+ // 1::6:7:8 1:2:3:4::6:7:8 1:2:3:4::8 + lelem+"{1,3}"+relem+"{1,4}|"+ // 1::5:6:7:8 1:2:3::5:6:7:8 1:2:3::8 + lelem+"{1,2}"+relem+"{1,5}|"+ // 1::4:5:6:7:8 1:2::4:5:6:7:8 1:2::8 + lelem +relem+"{1,6}|"+ // 1::3:4:5:6:7:8 1::3:4:5:6:7:8 1::8 + ":(?:"+relem+"{1,7}|:)|" + // ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 :: + lelem+"{6}"+ipv4+"|"+ // 1:2:3:4:5:6:10.0.0.1 + lelem+"{1,5}:"+ipv4+"|"+ // 1::10.0.0.1 1:2:3:4:5::10.0.0.1 + lelem+"{1,4}"+relem+":"+ipv4+"|"+ // 1::6:10.0.0.1 1:2:3:4::6:10.0.0.1 + lelem+"{1,3}"+relem+"{1,2}:"+ipv4+"|"+ // 1::5:6:10.0.0.1 1:2:3::5:6:10.0.0.1 1:2:3::6:10.0.0.1 + lelem+"{1,2}"+relem+"{1,3}:"+ipv4+"|"+ // 1::4:5:6:10.0.0.1 1:2::4:5:6:10.0.0.1 1:2::6:10.0.0.1 + lelem +relem+"{1,4}:"+ipv4+"|"+ // 1::3:4:5:6:10.0.0.1 1::3:4:5:6:10.0.0.1 1::6:10.0.0.1 + "::"+lelem+"{1,5}"+ipv4+ // ::2:3:4:5:10.0.0.1 ::5:10.0.0.1 ::10.0.0.1 + ")(?:%[0-9a-z]+)?" + + // This was part of the original regex, but is too specific to + // IPv6 details. + // fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}| # fe80::7:8%eth0 fe80::7:8%1 (link-local IPv6 addresses with zone index) + // ::(ffff(:0{1,4}){0,1}:){0,1} + // ((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3} + // (25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])| # ::255.255.255.255 ::ffff:255.255.255.255 ::ffff:0:255.255.255.255 (IPv4-mapped IPv6 addresses and IPv4-translated addresses) + // ([0-9a-fA-F]{1,4}:){1,4}: + // ((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3} + // (25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]) # 2001:db8:3:4::192.0.2.33 64:ff9b::192.0.2.33 (IPv4-Embedded IPv6 Address) + } + + private val ipv6Re = new RegExp("^"+ipv6address+"$", "i") + + // URI syntax parser. Based on RFC2396, RFC2732 and adaptations according to + // JavaDoc. + // - http://www.ietf.org/rfc/rfc2396.txt (see Appendix A for complete syntax) + // - http://www.ietf.org/rfc/rfc2732.txt + + private val uriRe = { + // We don't use any interpolators here to allow for constant folding + + /////////////////// + //// Helpers //// + /////////////////// + + // Inlined definitions + // reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | + // "$" | "," | "[" | "]" ; last two added by RFC2732 + // unreserved = alphanum | mark + // mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | + // "(" | ")" + + // escaped = "%" hex hex + val escaped = "%[a-f0-9]{2}" + + // uric = reserved | unreserved | escaped + val uric = "(?:[;/?:@&=+$,\\[\\]a-z0-9-_.!~*'()]|"+escaped+")" + + // pchar = unreserved | escaped | + // ":" | "@" | "&" | "=" | "+" | "$" | "," + val pchar = "(?:[a-z0-9-_.!~*'():@&=+$,]|"+escaped+")" + + /////////////////// + //// Server //// + /////////////////// + + // domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum + val domainlabel = "(?:[a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])" + + // toplabel = alpha | alpha *( alphanum | "-" ) alphanum + val toplabel = "(?:[a-z]|[a-z][a-z0-9-]*[a-z0-9])" + + // hostname = *( domainlabel "." ) toplabel [ "." ] + val hostname = "(?:"+domainlabel+"\\.)*"+toplabel+"\\.?" + + // IPv6reference = "[" IPv6address "]" + val ipv6reference = "\\[(?:"+ipv6address+")\\]" + + // host = hostname | IPv4address | IPv6reference + // ; IPv6reference added by RFC2732 + val host = "("+hostname+"|"+ipv4address+"|"+ipv6reference+")" /*CAPT*/ + + // Inlined definition + // port = *digit + + // hostport = host [ ":" port ] + val hostport = host+"(?::([0-9]*))?" /*CAPT*/ + + // userinfo = *( unreserved | escaped | + // ";" | ":" | "&" | "=" | "+" | "$" | "," ) + val userinfo = "(?:[a-z0-9-_.!~*'();:&=+$,]|"+escaped+")*" + + // server = [ [ userinfo "@" ] hostport ] + val server = "(?:(?:("+userinfo+")@)?"+hostport+")?" /*CAPT*/ + + /////////////////// + //// Authority //// + /////////////////// + + // reg_name = 1*( unreserved | escaped | "$" | "," | + // ";" | ":" | "@" | "&" | "=" | "+" ) + val reg_name = "(?:[a-z0-9-_.!~*'()$,;:@&=+]|"+escaped+")+" + + // authority = server | reg_name + val authority = server+"|"+reg_name + + /////////////////// + //// Paths //// + /////////////////// + + // Inlined definitions + // param = *pchar + + // segment = *pchar *( ";" param ) + val segment = pchar+"*(?:;"+pchar+"*)*" + + // path_segments = segment *( "/" segment ) + val path_segments = segment+"(?:/"+segment+")*" + + // abs_path = "/" path_segments + val abs_path = "/"+path_segments + + // net_path = "//" authority [ abs_path ] + val net_path = "//("+authority+")("+abs_path+")?" /*2CAPT*/ + + // Inlined definition + // Deviation from RCF2396 according to JavaDoc: Allow empty rel_segment + // and hence empty rel_path + // rel_segment = 1*( unreserved | escaped | + // ";" | "@" | "&" | "=" | "+" | "$" | "," ) + + // rel_path = rel_segment [ abs_path ] + val rel_path = "(?:[a-z0-9-_.!~*'();@&=+$,]|"+escaped+")*(?:"+abs_path+")?" + + /////////////////// + /// Query/Frag /// + /////////////////// + + // query = *uric + val query = "("+uric+"*)" /*CAPT*/ + // fragment = *uric + val fragment = "("+uric+"*)" /*CAPT*/ + + /////////////////// + /// Parts /// + /////////////////// + + // hier_part = ( net_path | abs_path ) [ "?" query ] + val hier_part = "(?:"+net_path+"|("+abs_path+"))(?:\\?"+query+")?" /*CAPT*/ + + // Inlined definition + // uric_no_slash = unreserved | escaped | ";" | "?" | ":" | "@" | + // "&" | "=" | "+" | "$" | "," + + // opaque_part = uric_no_slash *uric + val opaque_part = "(?:[a-z0-9-_.!~*'();?:@&=+$,]|"+escaped+")"+uric+"*" + + /////////////////// + /// URIs /// + /////////////////// + + // scheme = alpha *( alpha | digit | "+" | "-" | "." ) + val scheme = "([a-z][a-z0-9+-.]*)" /*CAPT*/ + + // absoluteURI = scheme ":" ( hier_part | opaque_part ) + val absoluteURI = scheme+":(?:("+hier_part+")|("+opaque_part+"))" /*2CAPT*/ + + // relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ] + val relativeURI = /*2CAPT*/ + "(?:"+net_path+"|("+abs_path+")|("+rel_path+"))(?:\\?"+query+")?" + + // URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ] + val uriRef = "^(?:"+absoluteURI+"|"+relativeURI+")(?:#"+fragment+")?$" + + new RegExp(uriRef, "i") + } + + private object Fields { + final val AbsScheme = 1 + final val AbsHierPart = AbsScheme+1 + final val AbsAuthority = AbsHierPart+1 + final val AbsUserInfo = AbsAuthority+1 + final val AbsHost = AbsUserInfo+1 + final val AbsPort = AbsHost+1 + final val AbsNetPath = AbsPort+1 // abs_path part only + final val AbsAbsPath = AbsNetPath+1 + final val AbsQuery = AbsAbsPath+1 + final val AbsOpaquePart = AbsQuery+1 + final val RelSchemeSpecificPart = 0 // It's the whole string + final val RelAuthority = AbsOpaquePart+1 + final val RelUserInfo = RelAuthority+1 + final val RelHost = RelUserInfo+1 + final val RelPort = RelHost+1 + final val RelNetPath = RelPort+1 // abs_path part only + final val RelAbsPath = RelNetPath+1 + final val RelRelPath = RelAbsPath+1 + final val RelQuery = RelRelPath+1 + final val Fragment = RelQuery+1 + } + + // Helpers for constructors + + private def uriStr(scheme: String, ssp: String, fragment: String): String = { + var resStr = "" + + if (scheme != null) + resStr += scheme + ":" + + if (ssp != null) + resStr += quoteIllegal(ssp) + + if (fragment != null) + resStr += "#" + quoteIllegal(fragment) + + resStr + } + + private def uriStr(scheme: String, userInfo: String, host: String, port: Int, + path: String, query: String, fragment: String): String = { + var resStr = "" + + if (scheme != null) + resStr += scheme + ":" + + if (userInfo != null || host != null || port != -1) + resStr += "//" + + if (userInfo != null) + resStr += quoteUserInfo(userInfo) + "@" + + if (host != null) { + if (URI.ipv6Re.test(host)) + resStr += "[" + host + "]" + else + resStr += host + } + + if (port != -1) + resStr += ":" + port + + if (path != null) + resStr += quotePath(path) + + if (query != null) + resStr += "?" + quoteIllegal(query) + + if (fragment != null) + resStr += "#" + quoteIllegal(fragment) + + resStr + } + + private def uriStr(scheme: String, authority: String, path: String, + query: String, fragment: String) = { + var resStr = "" + + if (scheme != null) + resStr += scheme + ":" + + if (authority != null) + resStr += "//" + quoteAuthority(authority) + + if (path != null) + resStr += quotePath(path) + + if (query != null) + resStr += "?" + quoteIllegal(query) + + if (fragment != null) + resStr += "#" + quoteIllegal(fragment) + + resStr + } + + // Quote helpers + + private val quoteChar: js.Function1[String, String] = { (str: String) => + require(str.length == 1) + + val c = str.head.toInt + + if (c > 127) + throw new URISyntaxException(null, "Only ASCII allowed in URIs") + else + f"%%$c%02x" + } + + /** matches any character not in unreserved, punct, escaped or other */ + private val userInfoQuoteRe = + new RegExp("[^a-z0-9-_.!~*'(),;:$&+=%\\s]|%(?![0-9a-f]{2})", "ig") + + /** Quote any character not in unreserved, punct, escaped or other */ + private def quoteUserInfo(str: String) = + (str: js.prim.String).replace(userInfoQuoteRe, quoteChar) + + /** matches any character not in unreserved, punct, escaped, other or equal + * to '/' or '@' + */ + private val pathQuoteRe = + new RegExp("[^a-z0-9-_.!~*'(),;:$&+=%\\s@/]|%(?![0-9a-f]{2})", "ig") + + /** Quote any character not in unreserved, punct, escaped, other or equal + * to '/' or '@' + */ + private def quotePath(str: String) = + (str: js.prim.String).replace(pathQuoteRe, quoteChar) + + /** matches any character not in unreserved, punct, escaped, other or equal + * to '@', '[' or ']' + * The last two are different to how JavaDoc specifies, but hopefully yield + * the same behavior. (We shouldn't escape [], since they may occur + * in IPv6 addresses, but technically speaking they are in reserved + * due to RFC2732). + */ + private val authorityQuoteRe = + new RegExp("[^a-z0-9-_.!~*'(),;:$&+=%\\s@\\[\\]]|%(?![0-9a-f]{2})", "ig") + + /** Quote any character not in unreserved, punct, escaped, other or equal + * to '@' + */ + private def quoteAuthority(str: String) = + (str: js.prim.String).replace(authorityQuoteRe, quoteChar) + + /** matches any character not in unreserved, reserved, escaped or other */ + private val illegalQuoteRe = + new RegExp("[^a-z0-9-_.!~*'(),;:$&+=?/\\[\\]%\\s]|%(?![0-9a-f]{2})", "ig") + + /** Quote any character not in unreserved, reserved, escaped or other */ + private def quoteIllegal(str: String) = + (str: js.prim.String).replace(illegalQuoteRe, quoteChar) + + /** Case-sensitive comparison that is case-insensitive inside URI + * escapes. Will compare `a%A0` and `a%a0` as equal, but `a%A0` and + * `A%A0` as different. + */ + private def escapeAwareCompare(x: String, y: String): Int = { + @tailrec + def loop(i: Int): Int = { + if (i >= x.length || i >= y.length) + x.length - y.length + else { + val diff = x.charAt(i) - y.charAt(i) + if (diff != 0) diff + else if (x.charAt(i) == '%') { + // we need to do a CI compare for the next two characters + assert(x.length > i + 2, "Invalid escape in URI") + assert(y.length > i + 2, "Invalid escape in URI") + val cmp = + x.substring(i+1, i+3).compareToIgnoreCase(y.substring(i+1, i+3)) + if (cmp != 0) cmp + else loop(i+3) + } else loop(i+1) + } + } + + loop(0) + } + + /** Upper-cases all URI escape sequences in `str`. Used for hashing */ + private def normalizeEscapes(str: String): String = { + var i = 0 + var res = "" + while (i < str.length) { + if (str.charAt(i) == '%') { + assert(str.length > i + 2, "Invalid escape in URI") + res += str.substring(i, i+3).toUpperCase() + i += 3 + } else { + res += str.substring(i, i+1) + i += 1 + } + } + + res + } + + private final val uriSeed = 53722356 + +} |