Skip to content

Commit 2f58550

Browse files
author
lostflydev
committed
Implement URI for pure Wasm (scala-wasm#151)
Use RegExpImpl to abstract over js.RegExp / java.util.regex.Pattern: - URI parsing: restore _fld pattern using RegExpImpl.impl.exec/matches/ exists/getOrElse; remove parseURI helper - IPv6 detection: ipv6Re compiled via RegExpImpl.impl.compile; inline the test into uriStr - uriRe: single val with RegExpImpl.impl.compile at end of block - Quoting (5 patterns): use RegExpImpl.impl.replaceAll with java.util.function.Function; QuoteStrMapper object in object URI - Path normalization: String#split + ju.Arrays.asList/subList/ scalaOps.mkString RegExpImpl additions: - Flags object (Global/CaseInsensitive as Int) replaces String flags - compile(patternStr, flags: Int) overload - replaceAll(pattern, string, Function[String,String]) with jsReplace in JSRegExpImpl and Matcher.replaceAll in JavaRegExpImpl Unblock URITest in pure Wasm test suite. Add tests for previously untested code paths: multi-component constructors, IPv6 host, quoteIllegal, normalize edge cases
1 parent f9d70f0 commit 2f58550

4 files changed

Lines changed: 172 additions & 77 deletions

File tree

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

Lines changed: 68 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,13 @@
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 => ju}
20+
import java.util.ScalaOps._
21+
import java.util.regex.RegExpImpl
2322

2423
final class URI(origStr: String) extends Serializable with Comparable[URI] {
2524

@@ -32,14 +31,14 @@ final class URI(origStr: String) extends Serializable with Comparable[URI] {
3231
* This is a local val for the primary constructor. It is a val,
3332
* since we'll set it to null after initializing all fields.
3433
*/
35-
private[this] var _fld: RegExp.ExecResult = URI.uriRe.exec(origStr)
36-
if (_fld == null)
34+
private[this] var _fld = RegExpImpl.impl.exec(URI.uriRe, origStr)
35+
if (!RegExpImpl.impl.matches(_fld))
3736
throw new URISyntaxException(origStr, "Malformed URI")
3837

39-
private val _isAbsolute = undefOrIsDefined(_fld(AbsScheme))
40-
private val _isOpaque = undefOrIsDefined(_fld(AbsOpaquePart))
38+
private val _isAbsolute = RegExpImpl.impl.exists(_fld, AbsScheme)
39+
private val _isOpaque = RegExpImpl.impl.exists(_fld, AbsOpaquePart)
4140

42-
@inline private def fld(idx: Int): String = undefOrGetOrNull(_fld(idx))
41+
@inline private def fld(idx: Int): String = RegExpImpl.impl.getOrElse(_fld, idx, null)
4342

4443
@inline private def fld(absIdx: Int, relIdx: Int): String =
4544
if (_isAbsolute) fld(absIdx) else fld(relIdx)
@@ -93,7 +92,7 @@ final class URI(origStr: String) extends Serializable with Comparable[URI] {
9392
private val _fragment = fld(Fragment)
9493

9594
// End of default ctor. Unset helper field
96-
_fld = null
95+
_fld = null.asInstanceOf[RegExpImpl.impl.Repr]
9796

9897
def this(scheme: String, ssp: String, fragment: String) =
9998
this(URI.uriStr(scheme, ssp, fragment))
@@ -217,11 +216,9 @@ final class URI(origStr: String) extends Serializable with Comparable[URI] {
217216

218217
def normalize(): URI = if (_isOpaque || _path == null) this
219218
else {
220-
import js.JSStringOps._
221-
222219
val origPath = _path
223220

224-
val segments = origPath.jsSplit("/")
221+
val segments = origPath.split("/", -1)
225222

226223
// Step 1: Remove all "." segments
227224
// Step 2: Remove ".." segments preceded by non ".." segment until no
@@ -279,17 +276,13 @@ final class URI(origStr: String) extends Serializable with Comparable[URI] {
279276
}
280277
}
281278

282-
// Truncate `segments` at `outIdx`
283-
segments.length = outIdx
284-
285279
// Step 3: If path is relative and first segment contains ":", prepend "."
286280
// segment (according to JavaDoc). If the path is absolute, the first
287281
// segment is "" so the `contains(':')` returns false.
288-
if (outIdx != 0 && segments(0).contains(":"))
289-
segments.unshift(".")
290-
291-
// Now add all the segments from step 1, 2 and 3
292-
val newPath = segments.join("/")
282+
val prependDot = outIdx != 0 && segments(0).contains(":")
283+
val normalized =
284+
ju.Arrays.asList(segments).subList(0, outIdx).scalaOps.mkString("", "/", "")
285+
val newPath = if (prependDot) "./" + normalized else normalized
293286

294287
// Only create new instance if anything changed
295288
if (newPath == origPath)
@@ -437,7 +430,8 @@ object URI {
437430
// (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)
438431
}
439432

440-
private val ipv6Re = new RegExp("^" + ipv6address + "$", "i")
433+
private val ipv6Re =
434+
RegExpImpl.impl.compile("^" + ipv6address + "$", RegExpImpl.Flags.CaseInsensitive)
441435

442436
// URI syntax parser. Based on RFC2396, RFC2732 and adaptations according to
443437
// JavaDoc.
@@ -586,7 +580,7 @@ object URI {
586580
// URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
587581
val uriRef = "^(?:" + absoluteURI + "|" + relativeURI + ")(?:#" + fragment + ")?$"
588582

589-
new RegExp(uriRef, "i")
583+
RegExpImpl.impl.compile(uriRef, RegExpImpl.Flags.CaseInsensitive)
590584
}
591585

592586
private object Fields {
@@ -643,7 +637,7 @@ object URI {
643637
resStr += quoteUserInfo(userInfo) + "@"
644638

645639
if (host != null) {
646-
if (URI.ipv6Re.test(host))
640+
if (RegExpImpl.impl.matches(RegExpImpl.impl.exec(URI.ipv6Re, host)))
647641
resStr += "[" + host + "]"
648642
else
649643
resStr += host
@@ -753,7 +747,8 @@ object URI {
753747
}
754748
}
755749

756-
private val quoteStr: js.Function1[String, String] = { (str: String) =>
750+
/** Encode a matched string as percent-encoded UTF-8 bytes. */
751+
private def quoteStrFn(str: String): String = {
757752
val buf = StandardCharsets.UTF_8.encode(str)
758753

759754
var res = ""
@@ -765,39 +760,41 @@ object URI {
765760
res
766761
}
767762

763+
private object QuoteStrMapper extends ju.function.Function[String, String] {
764+
def apply(t: String): String = quoteStrFn(t)
765+
}
766+
768767
/** 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")
768+
private val userInfoQuoteRe = RegExpImpl.impl.compile(
769+
// !other = [\u0000-\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\u2028\u2029]
770+
// Char class is: [:!other:^a-z0-9-_.!~*'(),;:$&+=%]
771+
"[\u0000- \"#/<>?@\\[-\\^`{-}" +
772+
"\u007f-\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\u2028\u2029]|" +
773+
"%(?![0-9a-f]{2})",
774+
RegExpImpl.Flags.Global | RegExpImpl.Flags.CaseInsensitive
775+
)
776776

777777
/** 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-
}
778+
private def quoteUserInfo(str: String): String =
779+
RegExpImpl.impl.replaceAll(userInfoQuoteRe, str, QuoteStrMapper)
782780

783781
/** matches any character not in unreserved, punct, escaped, other or equal
784782
* to '/' or '@'
785783
*/
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")
784+
private val pathQuoteRe = RegExpImpl.impl.compile(
785+
// !other = [\u0000-\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\u2028\u2029]
786+
// Char class is: [:!other:^a-z0-9-_.!~*'(),;:$&+=%@/]
787+
"[\u0000- \"#<>?\\[-\\^`{-}" +
788+
"\u007f-\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\u2028\u2029]|" +
789+
"%(?![0-9a-f]{2})",
790+
RegExpImpl.Flags.Global | RegExpImpl.Flags.CaseInsensitive
791+
)
793792

794793
/** Quote any character not in unreserved, punct, escaped, other or equal
795794
* to '/' or '@'
796795
*/
797-
private def quotePath(str: String) = {
798-
import js.JSStringOps._
799-
str.jsReplace(pathQuoteRe, quoteStr)
800-
}
796+
private def quotePath(str: String): String =
797+
RegExpImpl.impl.replaceAll(pathQuoteRe, str, QuoteStrMapper)
801798

802799
/** matches any character not in unreserved, punct, escaped, other or equal
803800
* to '@', '[' or ']'
@@ -806,48 +803,45 @@ object URI {
806803
* in IPv6 addresses, but technically speaking they are in reserved
807804
* due to RFC2732).
808805
*/
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")
806+
private[this] lazy val authorityQuoteRe = RegExpImpl.impl.compile(
807+
// !other = [\u0000-\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\u2028\u2029]
808+
// Char class is: [:!other:^a-z0-9-_.!~*'(),;:$&+=%@\[\]]
809+
"[\u0000- \"#/<>?\\^`{-}" +
810+
"\u007f-\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\u2028\u2029]|" +
811+
"%(?![0-9a-f]{2})",
812+
RegExpImpl.Flags.Global | RegExpImpl.Flags.CaseInsensitive
813+
)
816814

817815
/** Quote any character not in unreserved, punct, escaped, other or equal
818816
* to '@'
819817
*/
820-
private def quoteAuthority(str: String) = {
821-
import js.JSStringOps._
822-
str.jsReplace(authorityQuoteRe, quoteStr)
823-
}
818+
private def quoteAuthority(str: String): String =
819+
RegExpImpl.impl.replaceAll(authorityQuoteRe, str, QuoteStrMapper)
824820

825821
/** 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")
822+
private val illegalQuoteRe = RegExpImpl.impl.compile(
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+
RegExpImpl.Flags.Global | RegExpImpl.Flags.CaseInsensitive
829+
)
833830

834831
/** 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-
}
832+
private def quoteIllegal(str: String): String =
833+
RegExpImpl.impl.replaceAll(illegalQuoteRe, str, QuoteStrMapper)
839834

840835
/** matches characters not in ASCII
841836
*
842837
* Note: It is important that the match is maximal, since we might encounter
843838
* surrogates that need to be encoded in one shot.
844839
*/
845-
private val nonASCIIQuoteRe = new RegExp("[^\u0000-\u007F]+", "g")
840+
private val nonASCIIQuoteRe =
841+
RegExpImpl.impl.compile("[^\u0000-\u007F]+", RegExpImpl.Flags.Global)
846842

847-
private def quoteNonASCII(str: String) = {
848-
import js.JSStringOps._
849-
str.jsReplace(nonASCIIQuoteRe, quoteStr)
850-
}
843+
private def quoteNonASCII(str: String): String =
844+
RegExpImpl.impl.replaceAll(nonASCIIQuoteRe, str, QuoteStrMapper)
851845

852846
/** Case-insensitive comparison that accepts `null` values.
853847
*

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

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

3030
def compile(patternStr: String): PatRepr
3131
def compile(patternStr: String, global: Boolean): PatRepr
32+
def compile(patternStr: String, flags: Int): PatRepr
3233
def exec(pattern: PatRepr, string: String): Repr
3334
def matches(r: Repr): Boolean
3435
def exists(r: Repr, index: Int): Boolean
@@ -37,9 +38,20 @@ private[java] sealed abstract class RegExpImpl {
3738
def execFrom(pattern: PatRepr, string: String, startPos: Int): Repr
3839
def matchStart(r: Repr): Int
3940
def matchEnd(pattern: PatRepr, r: Repr): Int
41+
42+
def replaceAll(
43+
pattern: PatRepr,
44+
string: String,
45+
replacer: java.util.function.Function[String, String]
46+
): String
4047
}
4148

4249
private[java] object RegExpImpl {
50+
object Flags {
51+
final val Global = 0x01
52+
final val CaseInsensitive = 0x02
53+
}
54+
4355
val impl = LinkingInfo.linkTimeIf[RegExpImpl](
4456
moduleKind == MinimalWasmModule || moduleKind == WasmComponent) {
4557
JavaRegExpImpl
@@ -61,6 +73,14 @@ private[java] object RegExpImpl {
6173
else new js.RegExp(patternStr)
6274
}
6375

76+
def compile(patternStr: String, flags: Int): PatRepr = {
77+
val jsFlags = {
78+
(if ((flags & Flags.Global) != 0) "g" else "") +
79+
(if ((flags & Flags.CaseInsensitive) != 0) "i" else "")
80+
}
81+
new js.RegExp(patternStr, jsFlags)
82+
}
83+
6484
def exec(pattern: PatRepr, string: String): Repr = pattern.exec(string)
6585
def matches(r: Repr): Boolean = r != null
6686
def exists(r: Repr, index: Int): Boolean = undefOrIsDefined(r(index))
@@ -83,6 +103,20 @@ private[java] object RegExpImpl {
83103
if (r == null) -1
84104
else pattern.lastIndex
85105
}
106+
107+
def replaceAll(
108+
pattern: PatRepr,
109+
string: String,
110+
replacer: java.util.function.Function[String, String]
111+
): String = {
112+
import js.JSStringOps._
113+
if (!pattern.global)
114+
throw new IllegalArgumentException("replaceAll requires a global pattern")
115+
val jsFunc: js.Function1[String, String] = { (matched: String) =>
116+
replacer.apply(matched)
117+
}
118+
string.jsReplace(pattern, jsFunc)
119+
}
86120
}
87121

88122
private object JavaRegExpImpl extends RegExpImpl {
@@ -91,6 +125,14 @@ private[java] object RegExpImpl {
91125

92126
def compile(patternStr: String): PatRepr = Pattern.compile(patternStr)
93127
def compile(patternStr: String, global: Boolean): PatRepr = Pattern.compile(patternStr)
128+
129+
def compile(patternStr: String, flags: Int): PatRepr = {
130+
var patFlags = 0
131+
if ((flags & Flags.CaseInsensitive) != 0)
132+
patFlags |= Pattern.CASE_INSENSITIVE
133+
Pattern.compile(patternStr, patFlags)
134+
}
135+
94136
def exec(pattern: PatRepr, string: String): Repr = pattern.matcher(string)
95137
def matches(r: Repr): Boolean = r.matches()
96138
def exists(r: Repr, index: Int): Boolean = r.group(index) != null
@@ -115,5 +157,15 @@ private[java] object RegExpImpl {
115157
if (r == null) -1
116158
else r.end()
117159
}
160+
161+
def replaceAll(
162+
pattern: PatRepr,
163+
string: String,
164+
replacer: java.util.function.Function[String, String]
165+
): String = {
166+
pattern.matcher(string).replaceAll(new java.util.function.Function[MatchResult, String] {
167+
def apply(result: MatchResult): String = replacer.apply(result.group())
168+
})
169+
}
118170
}
119171
}

project/Build.scala

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2361,10 +2361,8 @@ object Build {
23612361
contains(f, "/shared/src/test/scala/org/scalajs/testsuite/") && (
23622362
// javalib/util
23632363
!endsWith(f, "/DateTest.scala") && // js.Date
2364-
!endsWith(f, "/PropertiesTest.scala") && // Date.toString
2364+
!endsWith(f, "/PropertiesTest.scala") // Date.toString
23652365

2366-
// javalib/net
2367-
!endsWith(f, "/net/URITest.scala") // URI.normalize
23682366
) ||
23692367
contains(f, "/js/src/test/scala/org/scalajs/testsuite/") && (
23702368
// compiler

0 commit comments

Comments
 (0)