summary refs log tree commit diff stats
path: root/lib/pure/uri.nim
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pure/uri.nim')
-rw-r--r--lib/pure/uri.nim624
1 files changed, 212 insertions, 412 deletions
diff --git a/lib/pure/uri.nim b/lib/pure/uri.nim
index b163a2ab4..725d5bbd9 100644
--- a/lib/pure/uri.nim
+++ b/lib/pure/uri.nim
@@ -11,42 +11,40 @@
 ##
 ## A Uniform Resource Identifier (URI) provides a simple and extensible
 ## means for identifying a resource. A URI can be further classified
-## as a locator, a name, or both. The term “Uniform Resource Locator”
+## as a locator, a name, or both. The term "Uniform Resource Locator"
 ## (URL) refers to the subset of URIs.
 ##
-## Basic usage
-## ===========
+## .. warning:: URI parsers in this module do not perform security validation.
 ##
-## Combine URIs
-## -------------
-## .. code-block::
-##    import uri
-##    let host = parseUri("https://nim-lang.org")
-##    let blog = "/blog.html"
-##    let bloguri = host / blog
-##    assert $host == "https://nim-lang.org"
-##    assert $bloguri == "https://nim-lang.org/blog.html"
-##
-## Access URI item
-## ---------------
-## .. code-block::
-##    import uri
-##    let res = parseUri("sftp://127.0.0.1:4343")
-##    if isAbsolute(res):
-##      assert res.port == "4343"
-##    else:
-##      echo "Wrong format"
-##
-## Data URI Base64
-## ---------------
-##
-## .. code-block::nim
-##    doAssert getDataUri("Hello World", "text/plain") == "data:text/plain;charset=utf-8;base64,SGVsbG8gV29ybGQ="
-##    doAssert getDataUri("Nim", "text/plain") == "data:text/plain;charset=utf-8;base64,Tmlt"
+## # Basic usage
+
+
+## ## Combine URIs
+runnableExamples:
+  let host = parseUri("https://nim-lang.org")
+  assert $host == "https://nim-lang.org"
+  assert $(host / "/blog.html") == "https://nim-lang.org/blog.html"
+  assert $(host / "blog2.html") == "https://nim-lang.org/blog2.html"
+
+## ## Access URI item
+runnableExamples:
+  let res = parseUri("sftp://127.0.0.1:4343")
+  assert isAbsolute(res)
+  assert res.port == "4343"
+
+## ## Data URI Base64
+runnableExamples:
+  assert getDataUri("Hello World", "text/plain") == "data:text/plain;charset=utf-8;base64,SGVsbG8gV29ybGQ="
+  assert getDataUri("Nim", "text/plain") == "data:text/plain;charset=utf-8;base64,Tmlt"
+
+
+import std/[strutils, parseutils, base64]
+import std/private/[since, decode_helpers]
+
+when defined(nimPreviewSlimSystem):
+  import std/assertions
 
-import std/private/since
 
-import strutils, parseutils, base64
 type
   Url* = distinct string
 
@@ -54,21 +52,29 @@ type
     scheme*, username*, password*: string
     hostname*, port*, path*, query*, anchor*: string
     opaque*: bool
+    isIpv6*: bool
+
+  UriParseError* = object of ValueError
+
+
+proc uriParseError*(msg: string) {.noreturn.} =
+  ## Raises a `UriParseError` exception with message `msg`.
+  raise newException(UriParseError, msg)
 
-proc encodeUrl*(s: string, usePlus = true): string =
+func encodeUrl*(s: string, usePlus = true): string =
   ## Encodes a URL according to RFC3986.
   ##
   ## This means that characters in the set
-  ## ``{'a'..'z', 'A'..'Z', '0'..'9', '-', '.', '_', '~'}`` are
+  ## `{'a'..'z', 'A'..'Z', '0'..'9', '-', '.', '_', '~'}` are
   ## carried over to the result.
-  ## All other characters are encoded as ``%xx`` where ``xx``
+  ## All other characters are encoded as `%xx` where `xx`
   ## denotes its hexadecimal value.
   ##
-  ## As a special rule, when the value of ``usePlus`` is true,
-  ## spaces are encoded as ``+`` instead of ``%20``.
+  ## As a special rule, when the value of `usePlus` is true,
+  ## spaces are encoded as `+` instead of `%20`.
   ##
   ## **See also:**
-  ## * `decodeUrl proc<#decodeUrl,string>`_
+  ## * `decodeUrl func<#decodeUrl,string>`_
   runnableExamples:
     assert encodeUrl("https://nim-lang.org") == "https%3A%2F%2Fnim-lang.org"
     assert encodeUrl("https://nim-lang.org/this is a test") == "https%3A%2F%2Fnim-lang.org%2Fthis+is+a+test"
@@ -84,29 +90,25 @@ proc encodeUrl*(s: string, usePlus = true): string =
       add(result, '%')
       add(result, toHex(ord(c), 2))
 
-proc decodeUrl*(s: string, decodePlus = true): string =
+func decodeUrl*(s: string, decodePlus = true): string =
   ## Decodes a URL according to RFC3986.
   ##
-  ## This means that any ``%xx`` (where ``xx`` denotes a hexadecimal
-  ## value) are converted to the character with ordinal number ``xx``,
+  ## This means that any `%xx` (where `xx` denotes a hexadecimal
+  ## value) are converted to the character with ordinal number `xx`,
   ## and every other character is carried over.
+  ## If `xx` is not a valid hexadecimal value, it is left intact.
   ##
-  ## As a special rule, when the value of ``decodePlus`` is true, ``+``
+  ## As a special rule, when the value of `decodePlus` is true, `+`
   ## characters are converted to a space.
   ##
   ## **See also:**
-  ## * `encodeUrl proc<#encodeUrl,string>`_
+  ## * `encodeUrl func<#encodeUrl,string>`_
   runnableExamples:
     assert decodeUrl("https%3A%2F%2Fnim-lang.org") == "https://nim-lang.org"
     assert decodeUrl("https%3A%2F%2Fnim-lang.org%2Fthis+is+a+test") == "https://nim-lang.org/this is a test"
     assert decodeUrl("https%3A%2F%2Fnim-lang.org%2Fthis%20is%20a%20test",
         false) == "https://nim-lang.org/this is a test"
-  proc handleHexChar(c: char, x: var int) {.inline.} =
-    case c
-    of '0'..'9': x = (x shl 4) or (ord(c) - ord('0'))
-    of 'a'..'f': x = (x shl 4) or (ord(c) - ord('a') + 10)
-    of 'A'..'F': x = (x shl 4) or (ord(c) - ord('A') + 10)
-    else: assert(false)
+    assert decodeUrl("abc%xyz") == "abc%xyz"
 
   result = newString(s.len)
   var i = 0
@@ -114,11 +116,7 @@ proc decodeUrl*(s: string, decodePlus = true): string =
   while i < s.len:
     case s[i]
     of '%':
-      var x = 0
-      handleHexChar(s[i+1], x)
-      handleHexChar(s[i+2], x)
-      inc(i, 2)
-      result[j] = chr(x)
+      result[j] = decodePercent(s, i)
     of '+':
       if decodePlus:
         result[j] = ' '
@@ -129,27 +127,28 @@ proc decodeUrl*(s: string, decodePlus = true): string =
     inc(j)
   setLen(result, j)
 
-proc encodeQuery*(query: openArray[(string, string)], usePlus = true,
-    omitEq = true): string =
+func encodeQuery*(query: openArray[(string, string)], usePlus = true,
+    omitEq = true, sep = '&'): string =
   ## Encodes a set of (key, value) parameters into a URL query string.
   ##
-  ## Every (key, value) pair is URL-encoded and written as ``key=value``. If the
-  ## value is an empty string then the ``=`` is omitted, unless ``omitEq`` is
+  ## Every (key, value) pair is URL-encoded and written as `key=value`. If the
+  ## value is an empty string then the `=` is omitted, unless `omitEq` is
   ## false.
-  ## The pairs are joined together by a ``&`` character.
+  ## The pairs are joined together by the `sep` character.
   ##
-  ## The ``usePlus`` parameter is passed down to the `encodeUrl` function that
+  ## The `usePlus` parameter is passed down to the `encodeUrl` function that
   ## is used for the URL encoding of the string values.
   ##
   ## **See also:**
-  ## * `encodeUrl proc<#encodeUrl,string>`_
+  ## * `encodeUrl func<#encodeUrl,string>`_
   runnableExamples:
     assert encodeQuery({: }) == ""
     assert encodeQuery({"a": "1", "b": "2"}) == "a=1&b=2"
     assert encodeQuery({"a": "1", "b": ""}) == "a=1&b"
+    assert encodeQuery({"a": "1", "b": ""}, omitEq = false, sep = ';') == "a=1;b="
   for elem in query:
-    # Encode the `key = value` pairs and separate them with a '&'
-    if result.len > 0: result.add('&')
+    # Encode the `key = value` pairs and separate them with 'sep'
+    if result.len > 0: result.add(sep)
     let (key, val) = elem
     result.add(encodeUrl(key, usePlus))
     # Omit the '=' if the value string is empty
@@ -157,7 +156,52 @@ proc encodeQuery*(query: openArray[(string, string)], usePlus = true,
       result.add('=')
       result.add(encodeUrl(val, usePlus))
 
-proc parseAuthority(authority: string, result: var Uri) =
+iterator decodeQuery*(data: string, sep = '&'): tuple[key, value: string] =
+  ## Reads and decodes the query string `data` and yields the `(key, value)` pairs
+  ## the data consists of. If compiled with `-d:nimLegacyParseQueryStrict`,
+  ## a `UriParseError` is raised when there is an unencoded `=` character in a decoded
+  ## value, which was the behavior in Nim < 1.5.1.
+  runnableExamples:
+    import std/sequtils
+    assert toSeq(decodeQuery("foo=1&bar=2=3")) == @[("foo", "1"), ("bar", "2=3")]
+    assert toSeq(decodeQuery("foo=1;bar=2=3", ';')) == @[("foo", "1"), ("bar", "2=3")]
+    assert toSeq(decodeQuery("&a&=b&=&&")) == @[("", ""), ("a", ""), ("", "b"), ("", ""), ("", "")]
+
+  proc parseData(data: string, i: int, field: var string, sep: char): int =
+    result = i
+    while result < data.len:
+      let c = data[result]
+      case c
+      of '%': add(field, decodePercent(data, result))
+      of '+': add(field, ' ')
+      of '&': break
+      else:
+        if c == sep: break
+        else: add(field, data[result])
+      inc(result)
+
+  var i = 0
+  var name = ""
+  var value = ""
+  # decode everything in one pass:
+  while i < data.len:
+    setLen(name, 0) # reuse memory
+    i = parseData(data, i, name, '=')
+    setLen(value, 0) # reuse memory
+    if i < data.len and data[i] == '=':
+      inc(i) # skip '='
+      when defined(nimLegacyParseQueryStrict):
+        i = parseData(data, i, value, '=')
+      else:
+        i = parseData(data, i, value, sep)
+    yield (name, value)
+    if i < data.len:
+      when defined(nimLegacyParseQueryStrict):
+        if data[i] != '&':
+          uriParseError("'&' expected at index '$#' for '$#'" % [$i, data])
+      inc(i)
+
+func parseAuthority(authority: string, result: var Uri) =
   var i = 0
   var inPort = false
   var inIPv6 = false
@@ -176,6 +220,7 @@ proc parseAuthority(authority: string, result: var Uri) =
         inPort = true
     of '[':
       inIPv6 = true
+      result.isIpv6 = true
     of ']':
       inIPv6 = false
     else:
@@ -185,8 +230,7 @@ proc parseAuthority(authority: string, result: var Uri) =
         result.hostname.add(authority[i])
     i.inc
 
-proc parsePath(uri: string, i: var int, result: var Uri) =
-
+func parsePath(uri: string, i: var int, result: var Uri) =
   i.inc parseUntil(uri, result.path, {'?', '#'}, i)
 
   # The 'mailto' scheme's PATH actually contains the hostname/username
@@ -202,31 +246,34 @@ proc parsePath(uri: string, i: var int, result: var Uri) =
     i.inc # Skip '#'
     i.inc parseUntil(uri, result.anchor, {}, i)
 
-proc initUri*(): Uri =
-  ## Initializes a URI with ``scheme``, ``username``, ``password``,
-  ## ``hostname``, ``port``, ``path``, ``query`` and ``anchor``.
+func initUri*(isIpv6 = false): Uri =
+  ## Initializes a URI with `scheme`, `username`, `password`,
+  ## `hostname`, `port`, `path`, `query`, `anchor` and `isIpv6`.
   ##
   ## **See also:**
   ## * `Uri type <#Uri>`_ for available fields in the URI type
   runnableExamples:
-    var uri2: Uri
-    assert initUri() == uri2
+    var uri2 = initUri(isIpv6 = true)
+    uri2.scheme = "tcp"
+    uri2.hostname = "2001:0db8:85a3:0000:0000:8a2e:0370:7334"
+    uri2.port = "8080"
+    assert $uri2 == "tcp://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8080"
   result = Uri(scheme: "", username: "", password: "", hostname: "", port: "",
-                path: "", query: "", anchor: "")
+                path: "", query: "", anchor: "", isIpv6: isIpv6)
 
-proc resetUri(uri: var Uri) =
+func resetUri(uri: var Uri) =
   for f in uri.fields:
     when f is string:
       f.setLen(0)
     else:
       f = false
 
-proc parseUri*(uri: string, result: var Uri) =
+func parseUri*(uri: string, result: var Uri) =
   ## Parses a URI. The `result` variable will be cleared before.
   ##
   ## **See also:**
   ## * `Uri type <#Uri>`_ for available fields in the URI type
-  ## * `initUri proc <#initUri>`_ for initializing a URI
+  ## * `initUri func <#initUri>`_ for initializing a URI
   runnableExamples:
     var res = initUri()
     parseUri("https://nim-lang.org/docs/manual.html", res)
@@ -238,9 +285,9 @@ proc parseUri*(uri: string, result: var Uri) =
   var i = 0
 
   # Check if this is a reference URI (relative URI)
-  let doubleSlash = uri.len > 1 and uri[1] == '/'
+  let doubleSlash = uri.len > 1 and uri[0] == '/' and uri[1] == '/'
   if i < uri.len and uri[i] == '/':
-    # Make sure ``uri`` doesn't begin with '//'.
+    # Make sure `uri` doesn't begin with '//'.
     if not doubleSlash:
       parsePath(uri, i, result)
       return
@@ -269,7 +316,7 @@ proc parseUri*(uri: string, result: var Uri) =
   # Path
   parsePath(uri, i, result)
 
-proc parseUri*(uri: string): Uri =
+func parseUri*(uri: string): Uri =
   ## Parses a URI and returns it.
   ##
   ## **See also:**
@@ -282,10 +329,18 @@ proc parseUri*(uri: string): Uri =
   result = initUri()
   parseUri(uri, result)
 
-proc removeDotSegments(path: string): string =
+func removeDotSegments(path: string): string =
+  ## Collapses `..` and `.` in `path` in a similar way as done in `os.normalizedPath`
+  ## Caution: this is buggy.
+  runnableExamples:
+    assert removeDotSegments("a1/a2/../a3/a4/a5/./a6/a7/.//./") == "a1/a3/a4/a5/a6/a7/"
+    assert removeDotSegments("http://www.ai.") == "http://www.ai."
+  # xxx adapt or reuse `pathnorm.normalizePath(path, '/')` to make this more reliable, but
+  # taking into account url specificities such as not collapsing leading `//` in scheme
+  # `https://`. see `turi` for failing tests.
   if path.len == 0: return ""
   var collection: seq[string] = @[]
-  let endsWithSlash = path[path.len-1] == '/'
+  let endsWithSlash = path.endsWith '/'
   var i = 0
   var currentSegment = ""
   while i < path.len:
@@ -299,7 +354,7 @@ proc removeDotSegments(path: string): string =
           discard collection.pop()
           i.inc 3
           continue
-      elif path[i+1] == '/':
+      elif i + 1 < path.len and path[i+1] == '/':
         i.inc 2
         continue
       currentSegment.add path[i]
@@ -312,7 +367,7 @@ proc removeDotSegments(path: string): string =
   result = collection.join("/")
   if endsWithSlash: result.add '/'
 
-proc merge(base, reference: Uri): string =
+func merge(base, reference: Uri): string =
   # http://tools.ietf.org/html/rfc3986#section-5.2.3
   if base.hostname != "" and base.path == "":
     '/' & reference.path
@@ -323,7 +378,7 @@ proc merge(base, reference: Uri): string =
     else:
       base.path[0 .. lastSegment] & reference.path
 
-proc combine*(base: Uri, reference: Uri): Uri =
+func combine*(base: Uri, reference: Uri): Uri =
   ## Combines a base URI with a reference URI.
   ##
   ## This uses the algorithm specified in
@@ -333,7 +388,7 @@ proc combine*(base: Uri, reference: Uri): Uri =
   ## URIs path affect the resulting URI.
   ##
   ## **See also:**
-  ## * `/ proc <#/,Uri,string>`_ for building URIs
+  ## * `/ func <#/,Uri,string>`_ for building URIs
   runnableExamples:
     let foo = combine(parseUri("https://nim-lang.org/foo/bar"), parseUri("/baz"))
     assert foo.path == "/baz"
@@ -374,11 +429,11 @@ proc combine*(base: Uri, reference: Uri): Uri =
     result.scheme = base.scheme
   result.anchor = reference.anchor
 
-proc combine*(uris: varargs[Uri]): Uri =
+func combine*(uris: varargs[Uri]): Uri =
   ## Combines multiple URIs together.
   ##
   ## **See also:**
-  ## * `/ proc <#/,Uri,string>`_ for building URIs
+  ## * `/ func <#/,Uri,string>`_ for building URIs
   runnableExamples:
     let foo = combine(parseUri("https://nim-lang.org/"), parseUri("docs/"),
         parseUri("manual.html"))
@@ -388,24 +443,22 @@ proc combine*(uris: varargs[Uri]): Uri =
   for i in 1 ..< uris.len:
     result = combine(result, uris[i])
 
-proc isAbsolute*(uri: Uri): bool =
+func isAbsolute*(uri: Uri): bool =
   ## Returns true if URI is absolute, false otherwise.
   runnableExamples:
-    let foo = parseUri("https://nim-lang.org")
-    assert isAbsolute(foo) == true
-    let bar = parseUri("nim-lang")
-    assert isAbsolute(bar) == false
+    assert parseUri("https://nim-lang.org").isAbsolute
+    assert not parseUri("nim-lang").isAbsolute
   return uri.scheme != "" and (uri.hostname != "" or uri.path != "")
 
-proc `/`*(x: Uri, path: string): Uri =
+func `/`*(x: Uri, path: string): Uri =
   ## Concatenates the path specified to the specified URIs path.
   ##
-  ## Contrary to the `combine proc <#combine,Uri,Uri>`_ you do not have to worry about
+  ## Contrary to the `combine func <#combine,Uri,Uri>`_ you do not have to worry about
   ## the slashes at the beginning and end of the path and URIs path
   ## respectively.
   ##
   ## **See also:**
-  ## * `combine proc <#combine,Uri,Uri>`_
+  ## * `combine func <#combine,Uri,Uri>`_
   runnableExamples:
     let foo = parseUri("https://nim-lang.org/foo/bar") / "/baz"
     assert foo.path == "/foo/bar/baz"
@@ -431,7 +484,7 @@ proc `/`*(x: Uri, path: string): Uri =
       result.path.add '/'
     result.path.add(path)
 
-proc `?`*(u: Uri, query: openArray[(string, string)]): Uri =
+func `?`*(u: Uri, query: openArray[(string, string)]): Uri =
   ## Concatenates the query parameters to the specified URI object.
   runnableExamples:
     let foo = parseUri("https://example.com") / "foo" ? {"bar": "qux"}
@@ -439,41 +492,65 @@ proc `?`*(u: Uri, query: openArray[(string, string)]): Uri =
   result = u
   result.query = encodeQuery(query)
 
-proc `$`*(u: Uri): string =
+func `$`*(u: Uri): string =
   ## Returns the string representation of the specified URI object.
   runnableExamples:
-    let foo = parseUri("https://nim-lang.org")
-    assert $foo == "https://nim-lang.org"
-  result = ""
-  if u.scheme.len > 0:
-    result.add(u.scheme)
-    if u.opaque:
-      result.add(":")
-    else:
-      result.add("://")
-  if u.username.len > 0:
-    result.add(u.username)
-    if u.password.len > 0:
-      result.add(":")
-      result.add(u.password)
-    result.add("@")
+    assert $parseUri("https://nim-lang.org") == "https://nim-lang.org"
+  # Get the len of all the parts.
+  let schemeLen = u.scheme.len
+  let usernameLen = u.username.len
+  let passwordLen = u.password.len
+  let hostnameLen = u.hostname.len
+  let portLen = u.port.len
+  let pathLen = u.path.len
+  let queryLen = u.query.len
+  let anchorLen = u.anchor.len
+  # Prepare a string that fits all the parts and all punctuation chars.
+  # 12 is the max len required by all possible punctuation chars.
+  result = newStringOfCap(
+    schemeLen + usernameLen + passwordLen + hostnameLen + portLen + pathLen + queryLen + anchorLen + 12
+  )
+  # Insert to result.
+  if schemeLen > 0:
+    result.add u.scheme
+    result.add ':'
+    if not u.opaque:
+      result.add '/'
+      result.add '/'
+  if usernameLen > 0:
+    result.add u.username
+    if passwordLen > 0:
+      result.add ':'
+      result.add u.password
+    result.add '@'
   if u.hostname.endsWith('/'):
-    result.add(u.hostname[0..^2])
+    if u.isIpv6:
+      result.add '['
+      result.add u.hostname[0 .. ^2]
+      result.add ']'
+    else:
+      result.add u.hostname[0 .. ^2]
   else:
-    result.add(u.hostname)
-  if u.port.len > 0:
-    result.add(":")
-    result.add(u.port)
-  if u.path.len > 0:
-    if u.hostname.len > 0 and u.path[0] != '/':
-      result.add('/')
-    result.add(u.path)
-  if u.query.len > 0:
-    result.add("?")
-    result.add(u.query)
-  if u.anchor.len > 0:
-    result.add("#")
-    result.add(u.anchor)
+    if u.isIpv6:
+      result.add '['
+      result.add u.hostname
+      result.add ']'
+    else:
+      result.add u.hostname
+  if portLen > 0:
+    result.add ':'
+    result.add u.port
+  if pathLen > 0:
+    if hostnameLen > 0 and u.path[0] != '/':
+      result.add '/'
+    result.add u.path
+  if queryLen > 0:
+    result.add '?'
+    result.add u.query
+  if anchorLen > 0:
+    result.add '#'
+    result.add u.anchor
+
 
 proc getDataUri*(data, mime: string, encoding = "utf-8"): string {.since: (1, 3).} =
   ## Convenience proc for `base64.encode` returns a standard Base64 Data URI (RFC-2397)
@@ -482,291 +559,14 @@ proc getDataUri*(data, mime: string, encoding = "utf-8"): string {.since: (1, 3)
   ## * `mimetypes <mimetypes.html>`_ for `mime` argument
   ## * https://tools.ietf.org/html/rfc2397
   ## * https://en.wikipedia.org/wiki/Data_URI_scheme
-  runnableExamples: static: doAssert getDataUri("Nim", "text/plain") == "data:text/plain;charset=utf-8;base64,Tmlt"
+  runnableExamples: static: assert getDataUri("Nim", "text/plain") == "data:text/plain;charset=utf-8;base64,Tmlt"
   assert encoding.len > 0 and mime.len > 0 # Must *not* be URL-Safe, see RFC-2397
-  result = "data:" & mime & ";charset=" & encoding & ";base64," & base64.encode(data)
-
-
-when isMainModule:
-  block:
-    const test1 = "abc\L+def xyz"
-    doAssert encodeUrl(test1) == "abc%0A%2Bdef+xyz"
-    doAssert decodeUrl(encodeUrl(test1)) == test1
-    doAssert encodeUrl(test1, false) == "abc%0A%2Bdef%20xyz"
-    doAssert decodeUrl(encodeUrl(test1, false), false) == test1
-    doAssert decodeUrl(encodeUrl(test1)) == test1
-
-  block:
-    let str = "http://localhost"
-    let test = parseUri(str)
-    doAssert test.path == ""
-
-  block:
-    let str = "http://localhost/"
-    let test = parseUri(str)
-    doAssert test.path == "/"
-
-  block:
-    let str = "http://localhost:8080/test"
-    let test = parseUri(str)
-    doAssert test.scheme == "http"
-    doAssert test.port == "8080"
-    doAssert test.path == "/test"
-    doAssert test.hostname == "localhost"
-    doAssert($test == str)
-
-  block:
-    let str = "foo://username:password@example.com:8042/over/there" &
-              "/index.dtb?type=animal&name=narwhal#nose"
-    let test = parseUri(str)
-    doAssert test.scheme == "foo"
-    doAssert test.username == "username"
-    doAssert test.password == "password"
-    doAssert test.hostname == "example.com"
-    doAssert test.port == "8042"
-    doAssert test.path == "/over/there/index.dtb"
-    doAssert test.query == "type=animal&name=narwhal"
-    doAssert test.anchor == "nose"
-    doAssert($test == str)
-
-  block:
-    # IPv6 address
-    let str = "foo://[::1]:1234/bar?baz=true&qux#quux"
-    let uri = parseUri(str)
-    doAssert uri.scheme == "foo"
-    doAssert uri.hostname == "::1"
-    doAssert uri.port == "1234"
-    doAssert uri.path == "/bar"
-    doAssert uri.query == "baz=true&qux"
-    doAssert uri.anchor == "quux"
-
-  block:
-    let str = "urn:example:animal:ferret:nose"
-    let test = parseUri(str)
-    doAssert test.scheme == "urn"
-    doAssert test.path == "example:animal:ferret:nose"
-    doAssert($test == str)
-
-  block:
-    let str = "mailto:username@example.com?subject=Topic"
-    let test = parseUri(str)
-    doAssert test.scheme == "mailto"
-    doAssert test.username == "username"
-    doAssert test.hostname == "example.com"
-    doAssert test.query == "subject=Topic"
-    doAssert($test == str)
-
-  block:
-    let str = "magnet:?xt=urn:sha1:72hsga62ba515sbd62&dn=foobar"
-    let test = parseUri(str)
-    doAssert test.scheme == "magnet"
-    doAssert test.query == "xt=urn:sha1:72hsga62ba515sbd62&dn=foobar"
-    doAssert($test == str)
-
-  block:
-    let str = "/test/foo/bar?q=2#asdf"
-    let test = parseUri(str)
-    doAssert test.scheme == ""
-    doAssert test.path == "/test/foo/bar"
-    doAssert test.query == "q=2"
-    doAssert test.anchor == "asdf"
-    doAssert($test == str)
-
-  block:
-    let str = "test/no/slash"
-    let test = parseUri(str)
-    doAssert test.path == "test/no/slash"
-    doAssert($test == str)
-
-  block:
-    let str = "//git@github.com:dom96/packages"
-    let test = parseUri(str)
-    doAssert test.scheme == ""
-    doAssert test.username == "git"
-    doAssert test.hostname == "github.com"
-    doAssert test.port == "dom96"
-    doAssert test.path == "/packages"
-
-  block:
-    let str = "file:///foo/bar/baz.txt"
-    let test = parseUri(str)
-    doAssert test.scheme == "file"
-    doAssert test.username == ""
-    doAssert test.hostname == ""
-    doAssert test.port == ""
-    doAssert test.path == "/foo/bar/baz.txt"
-
-  # Remove dot segments tests
-  block:
-    doAssert removeDotSegments("/foo/bar/baz") == "/foo/bar/baz"
-
-  # Combine tests
-  block:
-    let concat = combine(parseUri("http://google.com/foo/bar/"), parseUri("baz"))
-    doAssert concat.path == "/foo/bar/baz"
-    doAssert concat.hostname == "google.com"
-    doAssert concat.scheme == "http"
-
-  block:
-    let concat = combine(parseUri("http://google.com/foo"), parseUri("/baz"))
-    doAssert concat.path == "/baz"
-    doAssert concat.hostname == "google.com"
-    doAssert concat.scheme == "http"
-
-  block:
-    let concat = combine(parseUri("http://google.com/foo/test"), parseUri("bar"))
-    doAssert concat.path == "/foo/bar"
-
-  block:
-    let concat = combine(parseUri("http://google.com/foo/test"), parseUri("/bar"))
-    doAssert concat.path == "/bar"
-
-  block:
-    let concat = combine(parseUri("http://google.com/foo/test"), parseUri("bar"))
-    doAssert concat.path == "/foo/bar"
-
-  block:
-    let concat = combine(parseUri("http://google.com/foo/test/"), parseUri("bar"))
-    doAssert concat.path == "/foo/test/bar"
-
-  block:
-    let concat = combine(parseUri("http://google.com/foo/test/"), parseUri("bar/"))
-    doAssert concat.path == "/foo/test/bar/"
-
-  block:
-    let concat = combine(parseUri("http://google.com/foo/test/"), parseUri("bar/"),
-                         parseUri("baz"))
-    doAssert concat.path == "/foo/test/bar/baz"
-
-  # `/` tests
-  block:
-    let test = parseUri("http://example.com/foo") / "bar/asd"
-    doAssert test.path == "/foo/bar/asd"
-
-  block:
-    let test = parseUri("http://example.com/foo/") / "/bar/asd"
-    doAssert test.path == "/foo/bar/asd"
-
-  # removeDotSegments tests
-  block:
-    # empty test
-    doAssert removeDotSegments("") == ""
-
-  # bug #3207
-  block:
-    doAssert parseUri("http://qq/1").combine(parseUri("https://qqq")).`$` == "https://qqq"
-
-  # bug #4959
-  block:
-    let foo = parseUri("http://example.com") / "/baz"
-    doAssert foo.path == "/baz"
-
-  # bug found on stream 13/10/17
-  block:
-    let foo = parseUri("http://localhost:9515") / "status"
-    doAssert $foo == "http://localhost:9515/status"
-
-  # bug #6649 #6652
-  block:
-    var foo = parseUri("http://example.com")
-    foo.hostname = "example.com"
-    foo.path = "baz"
-    doAssert $foo == "http://example.com/baz"
-
-    foo.hostname = "example.com/"
-    foo.path = "baz"
-    doAssert $foo == "http://example.com/baz"
-
-    foo.hostname = "example.com"
-    foo.path = "/baz"
-    doAssert $foo == "http://example.com/baz"
-
-    foo.hostname = "example.com/"
-    foo.path = "/baz"
-    doAssert $foo == "http://example.com/baz"
-
-    foo.hostname = "example.com/"
-    foo.port = "8000"
-    foo.path = "baz"
-    doAssert $foo == "http://example.com:8000/baz"
-
-    foo = parseUri("file:/dir/file")
-    foo.path = "relative"
-    doAssert $foo == "file:relative"
-
-  # isAbsolute tests
-  block:
-    doAssert "www.google.com".parseUri().isAbsolute() == false
-    doAssert "http://www.google.com".parseUri().isAbsolute() == true
-    doAssert "file:/dir/file".parseUri().isAbsolute() == true
-    doAssert "file://localhost/dir/file".parseUri().isAbsolute() == true
-    doAssert "urn:ISSN:1535-3613".parseUri().isAbsolute() == true
-
-    # path-relative URL *relative
-    doAssert "about".parseUri().isAbsolute == false
-    doAssert "about/staff.html".parseUri().isAbsolute == false
-    doAssert "about/staff.html?".parseUri().isAbsolute == false
-    doAssert "about/staff.html?parameters".parseUri().isAbsolute == false
-
-    # absolute-path-relative URL *relative
-    doAssert "/".parseUri().isAbsolute == false
-    doAssert "/about".parseUri().isAbsolute == false
-    doAssert "/about/staff.html".parseUri().isAbsolute == false
-    doAssert "/about/staff.html?".parseUri().isAbsolute == false
-    doAssert "/about/staff.html?parameters".parseUri().isAbsolute == false
-
-    # scheme-relative URL *relative
-    doAssert "//username:password@example.com:8888".parseUri().isAbsolute == false
-    doAssert "//username@example.com".parseUri().isAbsolute == false
-    doAssert "//example.com".parseUri().isAbsolute == false
-    doAssert "//example.com/".parseUri().isAbsolute == false
-    doAssert "//example.com/about".parseUri().isAbsolute == false
-    doAssert "//example.com/about/staff.html".parseUri().isAbsolute == false
-    doAssert "//example.com/about/staff.html?".parseUri().isAbsolute == false
-    doAssert "//example.com/about/staff.html?parameters".parseUri().isAbsolute == false
-
-    # absolute URL *absolute
-    doAssert "https://username:password@example.com:8888".parseUri().isAbsolute == true
-    doAssert "https://username@example.com".parseUri().isAbsolute == true
-    doAssert "https://example.com".parseUri().isAbsolute == true
-    doAssert "https://example.com/".parseUri().isAbsolute == true
-    doAssert "https://example.com/about".parseUri().isAbsolute == true
-    doAssert "https://example.com/about/staff.html".parseUri().isAbsolute == true
-    doAssert "https://example.com/about/staff.html?".parseUri().isAbsolute == true
-    doAssert "https://example.com/about/staff.html?parameters".parseUri().isAbsolute == true
-
-  # encodeQuery tests
-  block:
-    doAssert encodeQuery({:}) == ""
-    doAssert encodeQuery({"foo": "bar"}) == "foo=bar"
-    doAssert encodeQuery({"foo": "bar & baz"}) == "foo=bar+%26+baz"
-    doAssert encodeQuery({"foo": "bar & baz"}, usePlus = false) == "foo=bar%20%26%20baz"
-    doAssert encodeQuery({"foo": ""}) == "foo"
-    doAssert encodeQuery({"foo": ""}, omitEq = false) == "foo="
-    doAssert encodeQuery({"a": "1", "b": "", "c": "3"}) == "a=1&b&c=3"
-    doAssert encodeQuery({"a": "1", "b": "", "c": "3"}, omitEq = false) == "a=1&b=&c=3"
-
-    block:
-      var foo = parseUri("http://example.com") / "foo" ? {"bar": "1", "baz": "qux"}
-      var foo1 = parseUri("http://example.com/foo?bar=1&baz=qux")
-      doAssert foo == foo1
-
-    block:
-      var foo = parseUri("http://example.com") / "foo" ? {"do": "do", "bar": ""}
-      var foo1 = parseUri("http://example.com/foo?do=do&bar")
-      doAssert foo == foo1
-
-  block dataUriBase64:
-    doAssert getDataUri("", "text/plain") == "data:text/plain;charset=utf-8;base64,"
-    doAssert getDataUri(" ", "text/plain") == "data:text/plain;charset=utf-8;base64,IA=="
-    doAssert getDataUri("c\xf7>", "text/plain") == "data:text/plain;charset=utf-8;base64,Y/c+"
-    doAssert getDataUri("Hello World", "text/plain") == "data:text/plain;charset=utf-8;base64,SGVsbG8gV29ybGQ="
-    doAssert getDataUri("leasure.", "text/plain") == "data:text/plain;charset=utf-8;base64,bGVhc3VyZS4="
-    doAssert getDataUri("""!@#$%^&*()_+""", "text/plain") == "data:text/plain;charset=utf-8;base64,IUAjJCVeJiooKV8r"
-    doAssert(getDataUri("the quick brown dog jumps over the lazy fox", "text/plain") ==
-      "data:text/plain;charset=utf-8;base64,dGhlIHF1aWNrIGJyb3duIGRvZyBqdW1wcyBvdmVyIHRoZSBsYXp5IGZveA==")
-    doAssert(getDataUri("""The present is theirs
-      The future, for which I really worked, is mine.""", "text/plain") ==
-      "data:text/plain;charset=utf-8;base64,VGhlIHByZXNlbnQgaXMgdGhlaXJzCiAgICAgIFRoZSBmdXR1cmUsIGZvciB3aGljaCBJIHJlYWxseSB3b3JrZWQsIGlzIG1pbmUu")
-
-  echo("All good!")
+  let base64encoded: string = base64.encode(data)
+  # ("data:".len + ";charset=".len + ";base64,".len) == 22
+  result = newStringOfCap(22 + mime.len + encoding.len + base64encoded.len)
+  result.add "data:"
+  result.add mime
+  result.add ";charset="
+  result.add encoding
+  result.add ";base64,"
+  result.add base64encoded