follow #15357 and move decodeQuery (#15860)

* follow #15357 and move decodeQuery
* solve problem one
* minor
* deprecate decodeData
* add changelog and since
* add testcase for decodeQuery
This commit is contained in:
flywind 2020-12-27 04:59:32 -06:00 • committed by GitHub
commit 689504081f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 67 additions and 34 deletions

View file

@ -55,6 +55,7 @@
- `writeStackTrace` is available in JS backend now. - `writeStackTrace` is available in JS backend now.
- Added `decodeQuery` to `std/uri`.
- `strscans.scanf` now supports parsing single characters. - `strscans.scanf` now supports parsing single characters.
- `strscans.scanTuple` added which uses `strscans.scanf` internally, returning a tuple which can be unpacked for easier usage of `scanf`. - `strscans.scanTuple` added which uses `strscans.scanf` internally, returning a tuple which can be unpacked for easier usage of `scanf`.

View file

@ -32,8 +32,10 @@
import strutils, os, strtabs, cookies, uri import strutils, os, strtabs, cookies, uri
export uri.encodeUrl, uri.decodeUrl export uri.encodeUrl, uri.decodeUrl
import std/private/decode_helpers import std/private/decode_helpers
proc addXmlChar(dest: var string, c: char) {.inline.} = proc addXmlChar(dest: var string, c: char) {.inline.} =
case c case c
of '&': add(dest, "&") of '&': add(dest, "&")
@ -53,18 +55,15 @@ proc xmlEncode*(s: string): string =
for i in 0..len(s)-1: addXmlChar(result, s[i]) for i in 0..len(s)-1: addXmlChar(result, s[i])
type 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 RequestMethod* = enum ## the used request method
methodNone, ## no REQUEST_METHOD environment variable methodNone, ## no REQUEST_METHOD environment variable
methodPost, ## query uses the POST method methodPost, ## query uses the POST method
methodGet ## query uses the GET method methodGet ## query uses the GET method
proc cgiError*(msg: string) {.noreturn.} = proc cgiError*(msg: string) {.noreturn.} =
## raises an ECgi exception with message `msg`. ## Raises a ``CgiError`` exception with message `msg`.
var e: ref CgiError raise newException(CgiError, msg)
new(e)
e.msg = msg
raise e
proc getEncodedData(allowedMethods: set[RequestMethod]): string = proc getEncodedData(allowedMethods: set[RequestMethod]): string =
case getEnv("REQUEST_METHOD").string case getEnv("REQUEST_METHOD").string
@ -88,40 +87,23 @@ proc getEncodedData(allowedMethods: set[RequestMethod]): string =
iterator decodeData*(data: string): tuple[key, value: TaintedString] = iterator decodeData*(data: string): tuple[key, value: TaintedString] =
## Reads and decodes CGI data and yields the (name, value) pairs the ## Reads and decodes CGI data and yields the (name, value) pairs the
## data consists of. ## data consists of.
proc parseData(data: string, i: int, field: var string): int = try:
result = i for (key, value) in uri.decodeQuery(data):
while result < data.len: yield (key, value)
case data[result] except UriParseError as e:
of '%': add(field, decodePercent(data, result)) cgiError(e.msg)
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")
iterator decodeData*(allowedMethods: set[RequestMethod] = iterator decodeData*(allowedMethods: set[RequestMethod] =
{methodNone, methodPost, methodGet}): tuple[key, value: TaintedString] = {methodNone, methodPost, methodGet}): tuple[key, value: TaintedString] =
## Reads and decodes CGI data and yields the (name, value) pairs the ## 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 ## 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) let data = getEncodedData(allowedMethods)
for key, value in decodeData(data): try:
for (key, value) in uri.decodeQuery(data):
yield (key, value) yield (key, value)
except UriParseError as e:
cgiError(e.msg)
proc readData*(allowedMethods: set[RequestMethod] = proc readData*(allowedMethods: set[RequestMethod] =
{methodNone, methodPost, methodGet}): StringTableRef = {methodNone, methodPost, methodGet}): StringTableRef =

View file

@ -59,6 +59,13 @@ type
opaque*: bool opaque*: bool
isIpv6: bool # not expose it for compatibility. isIpv6: bool # not expose it for compatibility.
UriParseError* = object of ValueError
proc uriParseError*(msg: string) {.noreturn.} =
## Raises a ``UriParseError`` exception with message `msg`.
raise newException(UriParseError, msg)
func encodeUrl*(s: string, usePlus = true): string = func encodeUrl*(s: string, usePlus = true): string =
## Encodes a URL according to RFC3986. ## Encodes a URL according to RFC3986.
## ##
@ -153,6 +160,42 @@ func encodeQuery*(query: openArray[(string, string)], usePlus = true,
result.add('=') result.add('=')
result.add(encodeUrl(val, usePlus)) result.add(encodeUrl(val, usePlus))
iterator decodeQuery*(data: string): tuple[key, value: TaintedString] =
## Reads and decodes query string ``data`` and yields the (key, value) pairs the
## data consists of.
runnableExamples:
import std/sugar
let s = collect(newSeq):
for k, v in decodeQuery("foo=1&bar=2"): (k, v)
doAssert s == @[("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 index '$#' for '$#'" % [$i, data])
func parseAuthority(authority: string, result: var Uri) = func parseAuthority(authority: string, result: var Uri) =
var i = 0 var i = 0
var inPort = false var inPort = false

View file

@ -0,0 +1,7 @@
import std/[uri, sequtils]
block:
doAssert toSeq(decodeQuery("a=1&b=0")) == @[("a", "1"), ("b", "0")]
doAssertRaises(UriParseError):
discard toSeq(decodeQuery("a=1&b=2c=6"))