2.89 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            xs *vc = xs_realloc(NULL, ps + 1);
316            memcpy(vc, payload + po, ps);
317            vc[ps] = '\0';
318
319            if (xs_is_string(vn) && xs_is_string(vc))
320                p_vars = xs_dict_append(p_vars, vn, vc);
321        }
322
323        /* move on */
324        offset = q - payload;
325    }
326
327    return p_vars;
328}
329
330
331#endif /* XS_IMPLEMENTATION */
332
333#endif /* XS_URL_H */