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