Skip to content

Commit dacf3e8

Browse files
author
lostflydev
committed
Implement URI for pure Wasm (scala-wasm#151)
Use RegExpImpl abstraction instead of direct linkTimeIf branches: - Add compile(patternStr, flags) overload to RegExpImpl - Refactor parseURI, testIPv6, and all 5 quoting functions to use RegExpImpl - Unify normalize() using String#split instead of jsSplit/js.Array - Remove all JS-specific imports from URI.scala Unblock URITest in pure Wasm test suite. Add tests for multi-component constructor quoting, IPv6 detection, dot-only path normalization, and empty segment normalization.
1 parent 557c646 commit dacf3e8

4 files changed

Lines changed: 197 additions & 77 deletions

File tree

javalib/src/main/scala/java/net/URI.scala

Lines changed: 133 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,11 @@
1212

1313
package java.net
1414

15-
import scala.scalajs.js.RegExp
16-
import scala.scalajs.js
17-
1815
import scala.annotation.tailrec
1916

20-
import java.lang.Utils._
2117
import java.nio._
2218
import java.nio.charset.{CodingErrorAction, StandardCharsets}
19+
import java.util.regex.RegExpImpl
2320

2421
final 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
*

javalib/src/main/scala/java/util/regex/RegExpImpl.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ private[java] sealed abstract class RegExpImpl {
2727

2828
def compile(patternStr: String): PatRepr
2929
def compile(patternStr: String, global: Boolean): PatRepr
30+
def compile(patternStr: String, flags: String): PatRepr
3031
def exec(pattern: PatRepr, string: String): Repr
3132
def matches(r: Repr): Boolean
3233
def exists(r: Repr, index: Int): Boolean
@@ -58,6 +59,9 @@ private[java] object RegExpImpl {
5859
else new js.RegExp(patternStr)
5960
}
6061

62+
def compile(patternStr: String, flags: String): PatRepr =
63+
new js.RegExp(patternStr, flags)
64+
6165
def exec(pattern: PatRepr, string: String): Repr = pattern.exec(string)
6266
def matches(r: Repr): Boolean = r != null
6367
def exists(r: Repr, index: Int): Boolean = undefOrIsDefined(r(index))
@@ -88,6 +92,13 @@ private[java] object RegExpImpl {
8892

8993
def compile(patternStr: String): PatRepr = Pattern.compile(patternStr)
9094
def compile(patternStr: String, global: Boolean): PatRepr = Pattern.compile(patternStr)
95+
96+
def compile(patternStr: String, flags: String): PatRepr = {
97+
var javaFlags = 0
98+
if (flags.contains("i")) javaFlags |= Pattern.CASE_INSENSITIVE
99+
Pattern.compile(patternStr, javaFlags)
100+
}
101+
91102
def exec(pattern: PatRepr, string: String): Repr = pattern.matcher(string)
92103
def matches(r: Repr): Boolean = r.matches()
93104
def exists(r: Repr, index: Int): Boolean = r.group(index) != null

project/Build.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2360,10 +2360,10 @@ object Build {
23602360

23612361
// javalib/util
23622362
!endsWith(f, "/DateTest.scala") && // js.Date
2363-
!endsWith(f, "/PropertiesTest.scala") && // Date.toString
2363+
!endsWith(f, "/PropertiesTest.scala") // Date.toString
23642364

23652365
// javalib/net
2366-
!endsWith(f, "/net/URITest.scala") // URI.normalize
2366+
// (URITest is now compatible with pure Wasm)
23672367
) ||
23682368
contains(f, "/js/src/test/scala/org/scalajs/testsuite/") && (
23692369
// compiler

0 commit comments

Comments
 (0)