Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

follow #15357 and move decodeQuery #15860

Merged
merged 15 commits into from
Dec 27, 2020
2 changes: 2 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@

- `writeStackTrace` is available in JS backend now.

- Added `decodeQuery` to `std/uri`.

## Language changes

- `nimscript` now handles `except Exception as e`.
Expand Down
50 changes: 16 additions & 34 deletions lib/pure/cgi.nim
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@
import strutils, os, strtabs, cookies, uri
export uri.encodeUrl, uri.decodeUrl


import std/private/decode_helpers


proc addXmlChar(dest: var string, c: char) {.inline.} =
case c
of '&': add(dest, "&")
Expand All @@ -53,18 +55,15 @@ proc xmlEncode*(s: string): string =
for i in 0..len(s)-1: addXmlChar(result, s[i])

type
CgiError* = object of IOError ## exception that is raised if a CGI error occurs
CgiError* = object of IOError ## Exception that is raised if a CGI error occurs
RequestMethod* = enum ## the used request method
methodNone, ## no REQUEST_METHOD environment variable
methodPost, ## query uses the POST method
methodGet ## query uses the GET method

proc cgiError*(msg: string) {.noreturn.} =
## raises an ECgi exception with message `msg`.
var e: ref CgiError
new(e)
e.msg = msg
raise e
## Raises a ``CgiError`` exception with message `msg`.
raise newException(CgiError, msg)

proc getEncodedData(allowedMethods: set[RequestMethod]): string =
case getEnv("REQUEST_METHOD").string
Expand All @@ -88,40 +87,23 @@ proc getEncodedData(allowedMethods: set[RequestMethod]): string =
iterator decodeData*(data: string): tuple[key, value: TaintedString] =
ringabout marked this conversation as resolved.
Show resolved Hide resolved
## Reads and decodes CGI data and yields the (name, value) pairs the
## data consists of.
proc parseData(data: string, i: int, field: var string): int =
result = i
while result < data.len:
case data[result]
of '%': add(field, decodePercent(data, result))
of '+': add(field, ' ')
of '=', '&': 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 '='
i = parseData(data, i, value)
yield (name.TaintedString, value.TaintedString)
if i < data.len:
if data[i] == '&': inc(i)
else: cgiError("'&' expected")
try:
for (key, value) in uri.decodeQuery(data):
yield (key, value)
except UriParseError as e:
cgiError(e.msg)

iterator decodeData*(allowedMethods: set[RequestMethod] =
{methodNone, methodPost, methodGet}): tuple[key, value: TaintedString] =
## Reads and decodes CGI data and yields the (name, value) pairs the
## data consists of. If the client does not use a method listed in the
## `allowedMethods` set, an `ECgi` exception is raised.
## `allowedMethods` set, a ``CgiError`` exception is raised.
let data = getEncodedData(allowedMethods)
for key, value in decodeData(data):
yield (key, value)
try:
for (key, value) in uri.decodeQuery(data):
yield (key, value)
except UriParseError as e:
cgiError(e.msg)
ringabout marked this conversation as resolved.
Show resolved Hide resolved

proc readData*(allowedMethods: set[RequestMethod] =
{methodNone, methodPost, methodGet}): StringTableRef =
Expand Down
43 changes: 43 additions & 0 deletions lib/pure/uri.nim
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ type
opaque*: bool
isIpv6: bool # not expose it for compatibility.

UriParseError* = object of ValueError
ringabout marked this conversation as resolved.
Show resolved Hide resolved


proc uriParseError*(msg: string) {.noreturn.} =
## Raises a ``UriParseError`` exception with message `msg`.
raise newException(UriParseError, msg)

func encodeUrl*(s: string, usePlus = true): string =
## Encodes a URL according to RFC3986.
##
Expand Down Expand Up @@ -153,6 +160,42 @@ func encodeQuery*(query: openArray[(string, string)], usePlus = true,
result.add('=')
result.add(encodeUrl(val, usePlus))

iterator decodeQuery*(data: string): tuple[key, value: TaintedString] =
## Reads and decodes query string ``data`` and yields the (name, value) pairs the
ringabout marked this conversation as resolved.
Show resolved Hide resolved
## data consists of.
runnableExamples:
var queryData: seq[(string, string)]
ringabout marked this conversation as resolved.
Show resolved Hide resolved
for (key, value) in decodeQuery("foo=1&bar=2"):
queryData.add((key, value))
doAssert queryData == @[("foo", "1"), ("bar", "2")]

proc parseData(data: string, i: int, field: var string): int =
result = i
while result < data.len:
case data[result]
of '%': add(field, decodePercent(data, result))
of '+': add(field, ' ')
of '=', '&': 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 '='
i = parseData(data, i, value)
yield (name.TaintedString, value.TaintedString)
if i < data.len:
if data[i] == '&': inc(i)
else:
uriParseError("'&' expected at " & $i)
ringabout marked this conversation as resolved.
Show resolved Hide resolved

func parseAuthority(authority: string, result: var Uri) =
var i = 0
var inPort = false
Expand Down
10 changes: 10 additions & 0 deletions tests/stdlib/tdecodequery.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import uri, sequtils
timotheecour marked this conversation as resolved.
Show resolved Hide resolved


block:
const queryString = "a=1&b=0"
const wrongQueryString = "a=1&b=2c=6"

doAssert toSeq(decodeQuery(queryString)) == @[("a", "1"), ("b", "0")]
ringabout marked this conversation as resolved.
Show resolved Hide resolved
doAssertRaises(UriParseError):
discard toSeq(decodeQuery(wrongQueryString))