2.93
xs_url.h
1/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
2
3#ifndef _XS_URL_H
4
5#define _XS_URL_H
6
7xs_str *xs_url_dec(const char *str);
8xs_str *xs_url_enc(const char *str);
9xs_str *xs_url_dec_emoji(const char *str);
10xs_dict *xs_url_vars(const char *str);
11xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *header);
12
13#ifdef XS_IMPLEMENTATION
14
15char *xs_url_dec_in(char *str, int qs)
16{
17 char *w = str;
18 char *r;
19
20 for (r = str; *r != '\0'; r++) {
21 switch (*r) {
22 case '%': {
23 unsigned hex;
24 if (!r[1] || !r[2])
25 return NULL;
26 if (sscanf(r + 1, "%2x", &hex) != 1)
27 return NULL;
28 *w++ = hex;
29 r += 2;
30 break;
31 }
32
33 case '+':
34 if (qs) {
35 *w++ = ' ';
36 break;
37 }
38 /* fall-through */
39 default:
40 *w++ = *r;
41 }
42 }
43
44 *w++ = '\0';
45 return str;
46}
47
48xs_str *xs_url_dec(const char *str)
49/* decodes an URL */
50{
51 xs_str *s = xs_str_new(NULL);
52
53 while (*str) {
54 if (!xs_is_string(str))
55 break;
56
57 if (*str == '%') {
58 unsigned int i;
59
60 if (sscanf(str + 1, "%02x", &i) == 1) {
61 unsigned char uc = i;
62
63 if (!xs_is_string((char *)&uc))
64 break;
65
66 s = xs_append_m(s, (char *)&uc, 1);
67 str += 2;
68 }
69 }
70 else
71 if (*str == '+')
72 s = xs_append_m(s, " ", 1);
73 else
74 s = xs_append_m(s, str, 1);
75
76 str++;
77 }
78
79 return s;
80}
81
82
83xs_str *xs_url_dec_emoji(const char *str)
84/* decodes an URL, returns NULL if not every char is an encoded hex */
85{
86 xs_str *s = xs_str_new(NULL);
87
88 while (*str) {
89 if (!xs_is_string(str))
90 break;
91
92 if (*str == '%') {
93 unsigned int i;
94
95 if (sscanf(str + 1, "%02x", &i) == 1) {
96 unsigned char uc = i;
97
98 if (!xs_is_string((char *)&uc))
99 break;
100
101 s = xs_append_m(s, (char *)&uc, 1);
102 str += 2;
103 }
104 }
105 else
106 return NULL;
107
108 str++;
109 }
110
111 return s;
112}
113
114
115xs_str *xs_url_enc(const char *str)
116/* URL-encodes a string (RFC 3986) */
117{
118 xs_str *s = xs_str_new(NULL);
119
120 while (*str) {
121 if (isalnum(*str) || strchr("-._~", *str)) {
122 s = xs_append_m(s, str, 1);
123 }
124 else {
125 char tmp[8];
126 snprintf(tmp, sizeof(tmp), "%%%02X", (unsigned char)*str);
127 s = xs_append_m(s, tmp, 3);
128 }
129
130 str++;
131 }
132
133 return s;
134}
135
136
137xs_dict *xs_url_vars(const char *str)
138/* parse url variables */
139{
140 xs_dict *vars;
141
142 vars = xs_dict_new();
143
144 if (xs_is_string(str)) {
145 xs *dup = xs_dup(str);
146 char *k;
147 char *saveptr;
148 for (k = strtok_r(dup, "&", &saveptr);
149 k;
150 k = strtok_r(NULL, "&", &saveptr)) {
151 char *v = strchr(k, '=');
152 if (!v)
153 continue;
154 *v++ = '\0';
155 k = xs_url_dec_in(k, 1);
156 v = xs_url_dec_in(v, 1);
157 if (!xs_is_string(k) || !xs_is_string(v))
158 continue;
159
160 const char *pv = xs_dict_get(vars, k);
161 if (!xs_is_null(pv)) {
162 /* there is a previous value: convert to a list and append */
163 xs *vlist = NULL;
164 if (xs_type(pv) == XSTYPE_LIST)
165 vlist = xs_dup(pv);
166 else {
167 vlist = xs_list_new();
168 vlist = xs_list_append(vlist, pv);
169 }
170
171 vlist = xs_list_append(vlist, v);
172 vars = xs_dict_set(vars, k, vlist);
173 }
174 else {
175 /* ends with []? force to always be a list */
176 if (xs_endswith(k, "[]")) {
177 xs *vlist = xs_list_new();
178 vlist = xs_list_append(vlist, v);
179 vars = xs_dict_append(vars, k, vlist);
180 }
181 else
182 vars = xs_dict_append(vars, k, v);
183 }
184 }
185 }
186
187 return vars;
188}
189
190
191xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *header)
192/* parses a multipart/form-data payload */
193{
194 xs *boundary = NULL;
195 int offset = 0;
196 int bsz;
197 char *p;
198
199 /* build the boundary string */
200 {
201 xs *l1 = xs_split(header, "=");
202
203 if (xs_list_len(l1) != 2)
204 return NULL;
205
206 xs *t_boundary = xs_dup(xs_list_get(l1, 1));
207
208 /* Tokodon sends the boundary header with double quotes surrounded */
209 if (xs_between("\"", t_boundary, "\"") != 0)
210 t_boundary = xs_strip_chars_i(t_boundary, "\"");
211
212 boundary = xs_fmt("--%s", t_boundary);
213 }
214
215 bsz = strlen(boundary);
216
217 xs_dict *p_vars = xs_dict_new();
218
219 /* iterate searching the boundaries */
220 while ((p = xs_memmem(payload + offset, p_size - offset, boundary, bsz)) != NULL) {
221 xs *vn = NULL;
222 xs *fn = NULL;
223 xs *ct = NULL;
224 char *q;
225 int po, ps;
226
227 /* final boundary? */
228 p += bsz;
229
230 if ((p - payload) + 2 > p_size || (p[0] == '-' && p[1] == '-'))
231 break;
232
233 /* skip the \r\n */
234 p += 2;
235
236 /* Tokodon sends also a Content-Type headers,
237 let's use it to determine the file type */
238 do {
239 xs *s1 = NULL;
240 xs *l1 = NULL;
241 if (p[0] == '\r' && p[1] == '\n')
242 break;
243 q = memchr(p, '\r', p_size - (p - payload));
244
245 /* unexpected formatting, fail immediately */
246 if (q == NULL)
247 return p_vars;
248
249 s1 = xs_realloc(NULL, q - p + 1);
250 memcpy(s1, p, q - p);
251 s1[q - p] = '\0';
252
253 if (xs_startswith(s1, "Content-Disposition") || xs_startswith(s1, "content-disposition")) {
254 /* split by " like a primitive man */
255 l1 = xs_split(s1, "\"");
256
257 /* get the variable name */
258 vn = xs_dup(xs_list_get(l1, 1));
259
260 /* is it an attached file? */
261 if (xs_list_len(l1) >= 4 && strcmp(xs_list_get(l1, 2), "; filename=") == 0) {
262 /* get the file name */
263 fn = xs_dup(xs_list_get(l1, 3));
264 }
265 }
266 else
267 if (xs_startswith(s1, "Content-Type") || xs_startswith(s1, "content-type")) {
268 l1 = xs_split(s1, ":");
269
270 if (xs_list_len(l1) >= 2) {
271 ct = xs_lstrip_chars_i(xs_dup(xs_list_get(l1, 1)), " ");
272 }
273 }
274
275 p += (q - p);
276 p += 2; // Skip /r/n
277 } while (1);
278
279 /* find the start of the part content */
280 if ((p = xs_memmem(p, p_size - (p - payload), "\r\n", 2)) == NULL)
281 break;
282
283 p += 2; // Skip empty line
284
285 /* find the next boundary */
286 if ((q = xs_memmem(p, p_size - (p - payload), boundary, bsz)) == NULL)
287 break;
288
289 po = p - payload;
290 ps = q - p - 2; /* - 2 because the final \r\n */
291
292 /* is it a filename? */
293 if (fn != NULL) {
294 /* p_var value is a list */
295 /* if filename has no extension and content-type is image, attach extension to the filename */
296 if (strchr(fn, '.') == NULL && ct && xs_startswith(ct, "image/")) {
297 char *ext = strchr(ct, '/');
298 ext++;
299 fn = xs_str_cat(xs_str_new(""), fn, ".", ext);
300 }
301
302 xs *l1 = xs_list_new();
303 xs *vpo = xs_number_new(po);
304 xs *vps = xs_number_new(ps);
305
306 l1 = xs_list_append(l1, fn);
307 l1 = xs_list_append(l1, vpo);
308 l1 = xs_list_append(l1, vps);
309
310 if (xs_is_string(vn))
311 p_vars = xs_dict_append(p_vars, vn, l1);
312 }
313 else {
314 /* regular variable; just copy */
315 if (ps > 0) {
316 xs *vc = xs_realloc(NULL, ps + 1);
317 memcpy(vc, payload + po, ps);
318 vc[ps] = '\0';
319
320 if (xs_is_string(vn) && xs_is_string(vc))
321 p_vars = xs_dict_append(p_vars, vn, vc);
322 }
323 }
324
325 /* move on */
326 offset = q - payload;
327 }
328
329 return p_vars;
330}
331
332
333#endif /* XS_IMPLEMENTATION */
334
335#endif /* XS_URL_H */