-
Notifications
You must be signed in to change notification settings - Fork 29
/
codec.clj
187 lines (165 loc) · 5.94 KB
/
codec.clj
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
(ns ring.util.codec
"Functions for encoding and decoding data."
(:require [clojure.string :as str])
(:import java.util.Map
clojure.lang.MapEntry
java.nio.charset.Charset
[java.net URLEncoder URLDecoder]
[java.util Base64 StringTokenizer]))
(defn assoc-conj
"Associate a key with a value in a map. If the key already exists in the map,
a vector of values is associated with the key."
[map key val]
(assoc map key
(if-let [cur (get map key)]
(if (vector? cur)
(conj cur val)
[cur val])
val)))
(defn- double-escape [^String x]
(.replace (.replace x "\\" "\\\\") "$" "\\$"))
(def ^:private string-replace-bug?
(= "x" (str/replace "x" #"." (fn [x] "$0"))))
(defmacro ^:no-doc fix-string-replace-bug [x]
(if string-replace-bug?
`(double-escape ~x)
x))
(def ^:private ^Charset utf-8 (Charset/forName "UTF-8"))
(defn- ^Charset to-charset [s]
(if (nil? s) utf-8 (if (string? s) (Charset/forName s) s)))
(defn percent-encode
"Percent-encode every character in the given string using either the specified
encoding, or UTF-8 by default."
([unencoded]
(percent-encode unencoded utf-8))
([^String unencoded encoding]
(->> (.getBytes unencoded (to-charset encoding))
(map (partial format "%%%02X"))
(str/join))))
(defn- parse-bytes ^bytes [encoded-bytes]
(let [encoded-len (count encoded-bytes)
bs (byte-array (/ encoded-len 3))]
(loop [encoded-index 1, byte-index 0]
(if (< encoded-index encoded-len)
(let [encoded-byte (subs encoded-bytes encoded-index (+ encoded-index 2))
b (.byteValue (Integer/valueOf encoded-byte 16))]
(aset bs byte-index b)
(recur (+ encoded-index 3) (inc byte-index)))
bs))))
(defn percent-decode
"Decode every percent-encoded character in the given string using the
specified encoding, or UTF-8 by default."
([encoded]
(percent-decode encoded utf-8))
([^String encoded encoding]
(let [encoding (to-charset encoding)]
(str/replace encoded
#"(?:%[A-Fa-f0-9]{2})+"
(fn [chars]
(-> (parse-bytes chars)
(String. encoding)
(fix-string-replace-bug)))))))
(defn url-encode
"Returns the url-encoded version of the given string, using either a specified
encoding or UTF-8 by default."
([unencoded]
(url-encode unencoded utf-8))
([unencoded encoding]
(let [encoding (to-charset encoding)]
(str/replace
unencoded
#"[^A-Za-z0-9_~.+-]+"
#(double-escape (percent-encode % encoding))))))
(defn ^String url-decode
"Returns the url-decoded version of the given string, using either a specified
encoding or UTF-8 by default. If the encoding is invalid, nil is returned."
([encoded]
(url-decode encoded utf-8))
([encoded encoding]
(percent-decode encoded (to-charset encoding))))
(defn base64-encode
"Encode an array of bytes into a base64 encoded string."
[^bytes unencoded]
(String. (.encode (Base64/getEncoder) unencoded)))
(defn base64-decode
"Decode a base64 encoded string into an array of bytes."
[^String encoded]
(.decode (Base64/getDecoder) encoded))
(defprotocol ^:no-doc FormEncodeable
(form-encode* [x encoding]))
(extend-protocol FormEncodeable
String
(form-encode* [^String unencoded ^String encoding]
(URLEncoder/encode unencoded (to-charset encoding)))
Map
(form-encode* [params encoding]
(letfn [(encode [x] (form-encode* x encoding))
(encode-param [k v] (str (encode (name k)) "=" (encode v)))]
(->> params
(mapcat
(fn [[k v]]
(cond
(sequential? v) (map #(encode-param k %) v)
(set? v) (sort (map #(encode-param k %) v))
:else (list (encode-param k v)))))
(str/join "&"))))
Object
(form-encode* [x encoding]
(form-encode* (str x) encoding))
nil
(form-encode* [x encoding] ""))
(defn form-encode
"Encode the supplied value into www-form-urlencoded format, often used in
URL query strings and POST request bodies, using the specified encoding.
If the encoding is not specified, it defaults to UTF-8"
([x]
(form-encode x utf-8))
([x encoding]
(form-encode* x encoding)))
(defn- form-encoded-chars? [^String s]
(or (.contains s "+") (.contains s "%")))
(defn form-decode-str
"Decode the supplied www-form-urlencoded string using the specified encoding,
or UTF-8 by default."
([encoded]
(form-decode-str encoded utf-8))
([^String encoded encoding]
(if (form-encoded-chars? encoded)
(try
(URLDecoder/decode encoded (to-charset encoding))
(catch Exception _ nil))
encoded)))
(defn- tokenized [s delim]
(reify clojure.lang.IReduceInit
(reduce [_ f init]
(let [tokenizer (StringTokenizer. s delim)]
(loop [result init]
(if (.hasMoreTokens tokenizer)
(recur (f result (.nextToken tokenizer)))
result))))))
(def ^:private ^:const kv-separator (int \=))
(defn- split-key-value-pair [^String s]
(let [i (.indexOf s kv-separator)]
(cond
(pos? i) (MapEntry. (.substring s 0 i) (.substring s (inc i)))
(zero? i) (MapEntry. "" (.substring s (inc i)))
:else (MapEntry. s ""))))
(defn form-decode
"Decode the supplied www-form-urlencoded string using the specified encoding,
or UTF-8 by default. If the encoded value is a string, a string is returned.
If the encoded value is a map of parameters, a map is returned."
([encoded]
(form-decode encoded utf-8))
([^String encoded encoding]
(if-not (.contains encoded "=")
(form-decode-str encoded encoding)
(reduce
(fn [m param]
(let [kv (split-key-value-pair param)
k (form-decode-str (key kv) encoding)
v (form-decode-str (val kv) encoding)]
(if (and k v)
(assoc-conj m k v)
m)))
{}
(tokenized encoded "&")))))