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 */