Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 40 additions & 2 deletions http.carp
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ Returns `(Maybe String)`.")
@"Service Unavailable"
@"Unknown")))

(doc Form "provides URL-encoded form body parsing.
(doc Form "provides URL-encoded form body parsing and serialization.

```
(match (Form.parse \"name=carp&version=1\")
Expand Down Expand Up @@ -580,7 +580,45 @@ per the `application/x-www-form-urlencoded` specification.")
v (form-unescape
&(String.byte-slice pair (Int.inc eq) (String.length pair)))]
(Map.put! &m &k &v)))))
(Result.Success m))))
(Result.Success m)))

(private form-escape)
(hidden form-escape)
(defn form-escape [s]
(let-do [parts &(String.split-by s &[\space])
escaped []]
(for [i 0 (Array.length parts)]
(Array.push-back! &escaped (URI.escape (Array.unsafe-nth parts i))))
(String.join "+" &escaped)))

(doc encode "encodes a `(Map String String)` as a URL-encoded form body.
Spaces become `+` and other special characters are percent-encoded.
This is the inverse of `parse`.")
(defn encode [m]
(Map.kv-reduce
&(fn [acc k v]
(let [pair (String.concat &[(form-escape k) @"=" (form-escape v)])]
(if (String.empty? &acc) pair (String.concat &[acc @"&" pair]))))
@""
m))

(doc parse-pairs "parses a URL-encoded form body into an
`(Array (Pair String String))`. Unlike `parse`, this preserves the order
of pairs and allows duplicate keys. Keys and values are percent-decoded.
The `+` character is decoded as space.")
(defn parse-pairs [s]
(let-do [result (the (Array (Pair String String)) [])
parts &(String.split-by s &[\&])]
(for [i 0 (Array.length parts)]
(let [part (Array.unsafe-nth parts i)
eq (String.index-of part \=)]
(if (= eq -1)
(Array.push-back! &result (Pair.init (form-unescape part) @""))
(let [k (form-unescape &(String.byte-slice part 0 eq))
v (form-unescape
&(String.byte-slice part (Int.inc eq) (String.length part)))]
(Array.push-back! &result (Pair.init k v))))))
(Result.Success result))))

(defmodule Request
(doc form-data "parses the request body as URL-encoded form data.
Expand Down
126 changes: 125 additions & 1 deletion test/http.carp
Original file line number Diff line number Diff line change
Expand Up @@ -356,4 +356,128 @@
(Request.form-data &(Request.get (URI.zero) [] {} @"")))
(Result.Success m) (= 0 (Map.length &m))
_ false)
"form-data on empty body returns empty map"))
"form-data on empty body returns empty map")

; ---------------------------------------------------------------------------
; Form.encode
; ---------------------------------------------------------------------------

(assert-equal test
"name=carp"
&(let-do [m (the (Map String String) {})]
(Map.put! &m "name" "carp")
(Form.encode &m))
"encode single pair")

(assert-equal test
"msg=hello+world"
&(let-do [m (the (Map String String) {})]
(Map.put! &m "msg" "hello world")
(Form.encode &m))
"encode space as +")

(assert-equal test
"x=%26"
&(let-do [m (the (Map String String) {})]
(Map.put! &m "x" "&")
(Form.encode &m))
"encode & as %26")

(assert-equal test
"k=a%2bb"
&(let-do [m (the (Map String String) {})]
(Map.put! &m "k" "a+b")
(Form.encode &m))
"encode literal + as %2b")

(assert-equal test
"key+name=val"
&(let-do [m (the (Map String String) {})]
(Map.put! &m "key name" "val")
(Form.encode &m))
"encode space in key as +")

; roundtrip: parse(encode(m)) == m
(assert-true test
(let-do [m (the (Map String String) {})]
(Map.put! &m "a" "1")
(match (the (Result (Map String String) String)
(Form.parse &(Form.encode &m)))
(Result.Success parsed) (= &parsed &m)
_ false))
"encode/parse roundtrip single pair")

; ---------------------------------------------------------------------------
; Form.parse-pairs
; ---------------------------------------------------------------------------

(assert-equal test
2
(match (the
(Result (Array (Pair String String)) String)
(Form.parse-pairs "name=carp&version=1"))
(Result.Success ps) (Array.length &ps)
_ 0)
"parse-pairs returns correct count")

(assert-true test
(match (the
(Result (Array (Pair String String)) String)
(Form.parse-pairs "name=carp&version=1"))
(Result.Success ps)
(and
(= (Pair.a (Array.unsafe-nth &ps 0)) "name")
(= (Pair.b (Array.unsafe-nth &ps 0)) "carp"))
_ false)
"parse-pairs first pair is correct")

(assert-true test
(match (the
(Result (Array (Pair String String)) String)
(Form.parse-pairs "name=carp&version=1"))
(Result.Success ps)
(and
(= (Pair.a (Array.unsafe-nth &ps 1)) "version")
(= (Pair.b (Array.unsafe-nth &ps 1)) "1"))
_ false)
"parse-pairs second pair is correct")

; preserves duplicate keys
(assert-equal test
3
(match (the
(Result (Array (Pair String String)) String)
(Form.parse-pairs "x=1&x=2&x=3"))
(Result.Success ps) (Array.length &ps)
_ 0)
"parse-pairs preserves duplicate keys")

; decodes + as space
(assert-true test
(match (the
(Result (Array (Pair String String)) String)
(Form.parse-pairs "msg=hello+world"))
(Result.Success ps) (= (Pair.b (Array.unsafe-nth &ps 0)) "hello world")
_ false)
"parse-pairs decodes + as space")

; handles key without value
(assert-true test
(match (the
(Result (Array (Pair String String)) String)
(Form.parse-pairs "flag"))
(Result.Success ps)
(and
(= (Pair.a (Array.unsafe-nth &ps 0)) "flag")
(= (Pair.b (Array.unsafe-nth &ps 0)) ""))
_ false)
"parse-pairs handles key without value")

; decodes percent-encoded values
(assert-true test
(match (the
(Result (Array (Pair String String)) String)
(Form.parse-pairs "x=%26%3D"))
(Result.Success ps) (= (Pair.b (Array.unsafe-nth &ps 0)) "&=")
_ false)
"parse-pairs decodes percent-encoded values"))
Loading