summaryrefslogtreecommitdiff
path: root/examples/scala-js/javalib/src/main/scala/java/net/URI.scala
blob: c969f55d8e34c73e7969e1c294d95d6834ac6089 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
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

}