1212
1313package java .net
1414
15- import scala .scalajs .js .RegExp
16- import scala .scalajs .js
17-
1815import scala .annotation .tailrec
1916
20- import java .lang .Utils ._
2117import java .nio ._
2218import java .nio .charset .{CodingErrorAction , StandardCharsets }
19+ import java .util .regex .RegExpImpl
2320
2421final class URI (origStr : String ) extends Serializable with Comparable [URI ] {
2522
@@ -32,14 +29,14 @@ final class URI(origStr: String) extends Serializable with Comparable[URI] {
3229 * This is a local val for the primary constructor. It is a val,
3330 * since we'll set it to null after initializing all fields.
3431 */
35- private [this ] var _fld : RegExp . ExecResult = URI .uriRe.exec (origStr)
36- if (_fld == null )
32+ private [this ] var _parsed : Array [ String ] = URI .parseURI (origStr)
33+ if (_parsed == null )
3734 throw new URISyntaxException (origStr, " Malformed URI" )
3835
39- private val _isAbsolute = undefOrIsDefined(_fld( AbsScheme ))
40- private val _isOpaque = undefOrIsDefined(_fld( AbsOpaquePart ))
36+ private val _isAbsolute = _parsed( AbsScheme ) != null
37+ private val _isOpaque = _parsed( AbsOpaquePart ) != null
4138
42- @ inline private def fld (idx : Int ): String = undefOrGetOrNull(_fld( idx) )
39+ @ inline private def fld (idx : Int ): String = _parsed( idx)
4340
4441 @ inline private def fld (absIdx : Int , relIdx : Int ): String =
4542 if (_isAbsolute) fld(absIdx) else fld(relIdx)
@@ -93,7 +90,7 @@ final class URI(origStr: String) extends Serializable with Comparable[URI] {
9390 private val _fragment = fld(Fragment )
9491
9592 // End of default ctor. Unset helper field
96- _fld = null
93+ _parsed = null
9794
9895 def this (scheme : String , ssp : String , fragment : String ) =
9996 this (URI .uriStr(scheme, ssp, fragment))
@@ -217,11 +214,10 @@ final class URI(origStr: String) extends Serializable with Comparable[URI] {
217214
218215 def normalize (): URI = if (_isOpaque || _path == null ) this
219216 else {
220- import js .JSStringOps ._
221-
222217 val origPath = _path
223218
224- val segments = origPath.jsSplit(" /" )
219+ // Use String#split which works on both JS and pure Wasm targets
220+ val segments = origPath.split(" /" , - 1 )
225221
226222 // Step 1: Remove all "." segments
227223 // Step 2: Remove ".." segments preceded by non ".." segment until no
@@ -279,17 +275,24 @@ final class URI(origStr: String) extends Serializable with Comparable[URI] {
279275 }
280276 }
281277
282- // Truncate `segments` at `outIdx`
283- segments.length = outIdx
284-
285278 // Step 3: If path is relative and first segment contains ":", prepend "."
286279 // segment (according to JavaDoc). If the path is absolute, the first
287280 // segment is "" so the `contains(':')` returns false.
288- if (outIdx != 0 && segments(0 ).contains(" :" ))
289- segments.unshift(" ." )
281+ val prependDot = outIdx != 0 && segments(0 ).contains(" :" )
290282
291- // Now add all the segments from step 1, 2 and 3
292- val newPath = segments.join(" /" )
283+ // Build the new path from segments[0..outIdx)
284+ val newPath = {
285+ val sb = new java.lang.StringBuilder ()
286+ if (prependDot)
287+ sb.append(" ." )
288+ var i = 0
289+ while (i < outIdx) {
290+ if (i != 0 || prependDot) sb.append(" /" )
291+ sb.append(segments(i))
292+ i += 1
293+ }
294+ sb.toString
295+ }
293296
294297 // Only create new instance if anything changed
295298 if (newPath == origPath)
@@ -437,14 +440,23 @@ object URI {
437440 // (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)
438441 }
439442
440- private val ipv6Re = new RegExp (" ^" + ipv6address + " $" , " i" )
443+ private val ipv6ReStr = " ^" + ipv6address + " $"
444+
445+ private [this ] lazy val ipv6RePat =
446+ RegExpImpl .impl.compile(ipv6ReStr, " i" )
447+
448+ /** Test whether a host string is an IPv6 address. */
449+ private def testIPv6 (host : String ): Boolean = {
450+ import RegExpImpl .impl
451+ impl.matches(impl.exec(ipv6RePat, host))
452+ }
441453
442454 // URI syntax parser. Based on RFC2396, RFC2732 and adaptations according to
443455 // JavaDoc.
444456 // - http://www.ietf.org/rfc/rfc2396.txt (see Appendix A for complete syntax)
445457 // - http://www.ietf.org/rfc/rfc2732.txt
446458
447- private val uriRe = {
459+ private val uriReStr = {
448460 // We don't use any interpolators here to allow for constant folding
449461
450462 // /////////////////
@@ -584,9 +596,30 @@ object URI {
584596 " ((?:" + net_path + " |(" + abs_path + " )|(" + rel_path + " ))(?:\\ ?" + query + " )?)"
585597
586598 // URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
587- val uriRef = " ^(?:" + absoluteURI + " |" + relativeURI + " )(?:#" + fragment + " )?$"
599+ " ^(?:" + absoluteURI + " |" + relativeURI + " )(?:#" + fragment + " )?$"
600+ }
601+
602+ private [this ] lazy val uriRePat =
603+ RegExpImpl .impl.compile(uriReStr, " i" )
588604
589- new RegExp (uriRef, " i" )
605+ /** Parse a URI string, returning an Array[String] of matched groups
606+ * (null for non-matching groups), or null if the string is not a valid URI.
607+ */
608+ private def parseURI (str : String ): Array [String ] = {
609+ import RegExpImpl .impl
610+ val result = impl.exec(uriRePat, str)
611+ if (! impl.matches(result)) {
612+ null
613+ } else {
614+ val len = Fields .Fragment + 1
615+ val arr = new Array [String ](len)
616+ var i = 0
617+ while (i < len) {
618+ arr(i) = impl.getOrElse(result, i, null )
619+ i += 1
620+ }
621+ arr
622+ }
590623 }
591624
592625 private object Fields {
@@ -643,7 +676,7 @@ object URI {
643676 resStr += quoteUserInfo(userInfo) + " @"
644677
645678 if (host != null ) {
646- if (URI .ipv6Re.test (host))
679+ if (testIPv6 (host))
647680 resStr += " [" + host + " ]"
648681 else
649682 resStr += host
@@ -753,7 +786,8 @@ object URI {
753786 }
754787 }
755788
756- private val quoteStr : js.Function1 [String , String ] = { (str : String ) =>
789+ /** Encode a matched string as percent-encoded UTF-8 bytes. */
790+ private def quoteStrFn (str : String ): String = {
757791 val buf = StandardCharsets .UTF_8 .encode(str)
758792
759793 var res = " "
@@ -765,39 +799,60 @@ object URI {
765799 res
766800 }
767801
802+ /** Replace all matches of a compiled regex pattern with percent-encoded form. */
803+ private def quoteReplace (str : String ,
804+ pattern : RegExpImpl .impl.PatRepr ): String = {
805+ import RegExpImpl .impl
806+ val sb = new java.lang.StringBuilder ()
807+ var lastEnd = 0
808+ var result = impl.execFrom(pattern, str, 0 )
809+ while (impl.matchStart(result) != - 1 ) {
810+ val start = impl.matchStart(result)
811+ val end = impl.matchEnd(pattern, result)
812+ sb.append(str, lastEnd, start)
813+ sb.append(quoteStrFn(str.substring(start, end)))
814+ lastEnd = end
815+ result = impl.execFrom(pattern, str, end)
816+ }
817+ sb.append(str, lastEnd, str.length)
818+ sb.toString
819+ }
820+
768821 /** matches any character not in unreserved, punct, escaped or other */
769- private val userInfoQuoteRe = new RegExp (
770- // !other = [\u0000-\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\u2028\u2029]
771- // Char class is: [:!other:^a-z0-9-_.!~*'(),;:$&+=%]
772- " [\u0000 - \" #/<>?@\\ [-\\ ^`{-}" +
773- " \u007f -\u00a0\u1680\u2000 -\u200a\u202f\u205f\u3000\u2028\u2029 ]|" +
774- " %(?![0-9a-f]{2})" ,
775- " ig" )
822+ private val userInfoQuoteReStr = {
823+ // !other = [\u0000-\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\u2028\u2029]
824+ // Char class is: [:!other:^a-z0-9-_.!~*'(),;:$&+=%]
825+ " [\u0000 - \" #/<>?@\\ [-\\ ^`{-}" +
826+ " \u007f -\u00a0\u1680\u2000 -\u200a\u202f\u205f\u3000\u2028\u2029 ]|" +
827+ " %(?![0-9a-f]{2})"
828+ }
829+
830+ private [this ] lazy val userInfoQuotePat =
831+ RegExpImpl .impl.compile(userInfoQuoteReStr, " ig" )
776832
777833 /** Quote any character not in unreserved, punct, escaped or other */
778- private def quoteUserInfo (str : String ) = {
779- import js .JSStringOps ._
780- str.jsReplace(userInfoQuoteRe, quoteStr)
781- }
834+ private def quoteUserInfo (str : String ): String =
835+ quoteReplace(str, userInfoQuotePat)
782836
783837 /** matches any character not in unreserved, punct, escaped, other or equal
784838 * to '/' or '@'
785839 */
786- private val pathQuoteRe = new RegExp (
787- // !other = [\u0000-\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\u2028\u2029]
788- // Char class is: [:!other:^a-z0-9-_.!~*'(),;:$&+=%@/]
789- " [\u0000 - \" #<>?\\ [-\\ ^`{-}" +
790- " \u007f -\u00a0\u1680\u2000 -\u200a\u202f\u205f\u3000\u2028\u2029 ]|" +
791- " %(?![0-9a-f]{2})" ,
792- " ig" )
840+ private val pathQuoteReStr = {
841+ // !other = [\u0000-\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\u2028\u2029]
842+ // Char class is: [:!other:^a-z0-9-_.!~*'(),;:$&+=%@/]
843+ " [\u0000 - \" #<>?\\ [-\\ ^`{-}" +
844+ " \u007f -\u00a0\u1680\u2000 -\u200a\u202f\u205f\u3000\u2028\u2029 ]|" +
845+ " %(?![0-9a-f]{2})"
846+ }
847+
848+ private [this ] lazy val pathQuotePat =
849+ RegExpImpl .impl.compile(pathQuoteReStr, " ig" )
793850
794851 /** Quote any character not in unreserved, punct, escaped, other or equal
795852 * to '/' or '@'
796853 */
797- private def quotePath (str : String ) = {
798- import js .JSStringOps ._
799- str.jsReplace(pathQuoteRe, quoteStr)
800- }
854+ private def quotePath (str : String ): String =
855+ quoteReplace(str, pathQuotePat)
801856
802857 /** matches any character not in unreserved, punct, escaped, other or equal
803858 * to '@', '[' or ']'
@@ -806,48 +861,51 @@ object URI {
806861 * in IPv6 addresses, but technically speaking they are in reserved
807862 * due to RFC2732).
808863 */
809- private val authorityQuoteRe = new RegExp (
810- // !other = [\u0000-\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\u2028\u2029]
811- // Char class is: [:!other:^a-z0-9-_.!~*'(),;:$&+=%@\[\]]
812- " [\u0000 - \" #/<>?\\ ^`{-}" +
813- " \u007f -\u00a0\u1680\u2000 -\u200a\u202f\u205f\u3000\u2028\u2029 ]|" +
814- " %(?![0-9a-f]{2})" ,
815- " ig" )
864+ private val authorityQuoteReStr = {
865+ // !other = [\u0000-\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\u2028\u2029]
866+ // Char class is: [:!other:^a-z0-9-_.!~*'(),;:$&+=%@\[\]]
867+ " [\u0000 - \" #/<>?\\ ^`{-}" +
868+ " \u007f -\u00a0\u1680\u2000 -\u200a\u202f\u205f\u3000\u2028\u2029 ]|" +
869+ " %(?![0-9a-f]{2})"
870+ }
871+
872+ private [this ] lazy val authorityQuotePat =
873+ RegExpImpl .impl.compile(authorityQuoteReStr, " ig" )
816874
817875 /** Quote any character not in unreserved, punct, escaped, other or equal
818876 * to '@'
819877 */
820- private def quoteAuthority (str : String ) = {
821- import js .JSStringOps ._
822- str.jsReplace(authorityQuoteRe, quoteStr)
823- }
878+ private def quoteAuthority (str : String ): String =
879+ quoteReplace(str, authorityQuotePat)
824880
825881 /** matches any character not in unreserved, reserved, escaped or other */
826- private val illegalQuoteRe = new RegExp (
827- // !other = [\u0000-\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\u2028\u2029]
828- // Char class is: [:!other:^a-z0-9-_.!~*'(),;:$&+=?/\\[\\]%]
829- " [\u0000 - \" #<>@\\ ^`{-}" +
830- " \u007f -\u00a0\u1680\u2000 -\u200a\u202f\u205f\u3000\u2028\u2029 ]|" +
831- " %(?![0-9a-f]{2})" ,
832- " ig" )
882+ private val illegalQuoteReStr = {
883+ // !other = [\u0000-\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\u2028\u2029]
884+ // Char class is: [:!other:^a-z0-9-_.!~*'(),;:$&+=?/\\[\\]%]
885+ " [\u0000 - \" #<>@\\ ^`{-}" +
886+ " \u007f -\u00a0\u1680\u2000 -\u200a\u202f\u205f\u3000\u2028\u2029 ]|" +
887+ " %(?![0-9a-f]{2})"
888+ }
889+
890+ private [this ] lazy val illegalQuotePat =
891+ RegExpImpl .impl.compile(illegalQuoteReStr, " ig" )
833892
834893 /** Quote any character not in unreserved, reserved, escaped or other */
835- private def quoteIllegal (str : String ) = {
836- import js .JSStringOps ._
837- str.jsReplace(illegalQuoteRe, quoteStr)
838- }
894+ private def quoteIllegal (str : String ): String =
895+ quoteReplace(str, illegalQuotePat)
839896
840897 /** matches characters not in ASCII
841898 *
842899 * Note: It is important that the match is maximal, since we might encounter
843900 * surrogates that need to be encoded in one shot.
844901 */
845- private val nonASCIIQuoteRe = new RegExp ( " [^\u0000 -\u007F ]+" , " g " )
902+ private val nonASCIIQuoteReStr = " [^\u0000 -\u007F ]+"
846903
847- private def quoteNonASCII (str : String ) = {
848- import js .JSStringOps ._
849- str.jsReplace(nonASCIIQuoteRe, quoteStr)
850- }
904+ private [this ] lazy val nonASCIIQuotePat =
905+ RegExpImpl .impl.compile(nonASCIIQuoteReStr, " g" )
906+
907+ private def quoteNonASCII (str : String ): String =
908+ quoteReplace(str, nonASCIIQuotePat)
851909
852910 /** Case-insensitive comparison that accepts `null` values.
853911 *
0 commit comments