2 n__http__form_8c
gullradriel edited this page 2026-10-29 20:54:40 +01:00

src/n_http_form.c

Strict percent-decoding, query strings and urlencoded forms (see n_http_form.h). More...

Structures

Name
struct _mp_param

Functions

Name
int _mp_text_ok(const char * s, size_t n)
int _mp_tchar(unsigned char c)
int _mp_part_headers(const char * h, size_t n, N_HTTP_FORM_PART * part)
int _mp_next_param(const char * v, size_t n, size_t * pos, _mp_param * p)
int _mp_lc(int c)
int _mp_ieq(const char * s, size_t n, const char * lit)
size_t _mp_find_delim(const char * b, size_t len, size_t from, const char * boundary, size_t blen, size_t * after, int * closing)
int _mp_bchar(unsigned char c)
int _form_hex(unsigned char c)
size_t _form_decode_into(const char * s, size_t len, int flags, char * dst)
char * _form_decode_dup(const char * s, size_t len, int flags, size_t * out_len)

Defines

Name
_MP_PREAMBLE_MAX

Detailed Description

Strict percent-decoding, query strings and urlencoded forms (see n_http_form.h).

Author: Castagnier Mickael

Version: 1.0

Date: 2026

Functions Documentation

function _mp_text_ok

static int _mp_text_ok(
    const char * s,
    size_t n
)

function _mp_tchar

static int _mp_tchar(
    unsigned char c
)

function _mp_part_headers

static int _mp_part_headers(
    const char * h,
    size_t n,
    N_HTTP_FORM_PART * part
)

function _mp_next_param

static int _mp_next_param(
    const char * v,
    size_t n,
    size_t * pos,
    _mp_param * p
)

function _mp_lc

static int _mp_lc(
    int c
)

function _mp_ieq

static int _mp_ieq(
    const char * s,
    size_t n,
    const char * lit
)

function _mp_find_delim

static size_t _mp_find_delim(
    const char * b,
    size_t len,
    size_t from,
    const char * boundary,
    size_t blen,
    size_t * after,
    int * closing
)

function _mp_bchar

static int _mp_bchar(
    unsigned char c
)

function _form_hex

static int _form_hex(
    unsigned char c
)

function _form_decode_into

static size_t _form_decode_into(
    const char * s,
    size_t len,
    int flags,
    char * dst
)

function _form_decode_dup

static char * _form_decode_dup(
    const char * s,
    size_t len,
    int flags,
    size_t * out_len
)

Macros Documentation

define _MP_PREAMBLE_MAX

#define _MP_PREAMBLE_MAX 1024

Source code

/*
 * Nilorea Library
 * Copyright (C) 2005-2026 Castagnier Mickael
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * permissions and limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 */


#include "nilorea/n_http_form.h"

#include "nilorea/n_common.h"
#include "nilorea/n_log.h"
#include "nilorea/n_str.h"

#include <stdlib.h>
#include <string.h>

/* Value of a hex digit, or -1. */
static int _form_hex(unsigned char c) {
    if (c >= '0' && c <= '9')
        return c - '0';
    if (c >= 'a' && c <= 'f')
        return c - 'a' + 10;
    if (c >= 'A' && c <= 'F')
        return c - 'A' + 10;
    return -1;
}

/* Decode [s, s+len) into dst (at least len + 1 bytes). Returns the decoded
 * length, or (size_t)-1 on a malformed escape or a decoded NUL. */
static size_t _form_decode_into(const char* s, size_t len, int flags, char* dst) {
    size_t i, o = 0;
    for (i = 0; i < len; i++) {
        unsigned char c = (unsigned char)s[i];
        if (c == '%') {
            int hi, lo;
            if (len - i < 3)
                return (size_t)-1;
            hi = _form_hex((unsigned char)s[i + 1]);
            lo = _form_hex((unsigned char)s[i + 2]);
            if (hi < 0 || lo < 0)
                return (size_t)-1;
            c = (unsigned char)((hi << 4) | lo);
            i += 2;
        } else if (c == '+' && (flags & N_HTTP_FORM_PLUS_SPACE)) {
            c = ' ';
        }
        if (c == '\0')
            return (size_t)-1;
        dst[o++] = (char)c;
    }
    dst[o] = '\0';
    return o;
}

/* Decode into a new NUL-terminated heap string. Returns NULL on invalid input. */
static char* _form_decode_dup(const char* s, size_t len, int flags, size_t* out_len) {
    char* dst = NULL;
    size_t n;
    Malloc(dst, char, len + 1);
    __n_assert(dst, return NULL);
    n = _form_decode_into(s, len, flags, dst);
    if (n == (size_t)-1 ||
        (!(flags & N_HTTP_FORM_RAW_BYTES) && !n_str_utf8_valid(dst, n))) {
        Free(dst);
        return NULL;
    }
    *out_len = n;
    return dst;
}

int n_http_percent_decode(const char* s, size_t len, int flags, N_STR** out) {
    char* dec = NULL;
    size_t n = 0;
    __n_assert(out, return FALSE);
    if (!s && len > 0)
        return FALSE;
    dec = _form_decode_dup(s ? s : "", len, flags, &n);
    if (!dec)
        return FALSE;
    if (!*out) {
        *out = new_nstr(n + 1);
        if (!*out) {
            Free(dec);
            return FALSE;
        }
    }
    if (n > 0 && !nstrcat_ex(out, dec, n, 1)) {
        Free(dec);
        return FALSE;
    }
    Free(dec);
    return TRUE;
}

void n_http_form_free(N_HTTP_FORM** form) {
    size_t i;
    if (!form || !*form)
        return;
    for (i = 0; i < (*form)->nb_fields; i++) {
        FreeNoLog((*form)->fields[i].name);
        FreeNoLog((*form)->fields[i].value);
    }
    FreeNoLog((*form)->fields);
    FreeNoLog((*form));
}

N_HTTP_FORM* n_http_form_parse(const char* s, size_t len, size_t max_fields, int flags) {
    N_HTTP_FORM* form = NULL;
    size_t i, start, npairs = 0;
    int dflags = (flags & N_HTTP_FORM_RAW_BYTES) | N_HTTP_FORM_PLUS_SPACE;

    if ((!s && len > 0) || max_fields == 0)
        return NULL;
    if (!s)
        s = "";
    /* count the non-empty pairs first: the cap is checked before any field
     * is allocated, so a flood of '&' cannot cost memory */
    for (i = 0, start = 0; i <= len; i++) {
        if (i == len || s[i] == '&') {
            if (i > start && ++npairs > max_fields) {
                n_log(LOG_DEBUG, "form: more than %zu fields", max_fields);
                return NULL;
            }
            start = i + 1;
        }
    }
    Malloc(form, N_HTTP_FORM, 1);
    __n_assert(form, return NULL);
    if (npairs > 0) {
        Malloc(form->fields, N_HTTP_FIELD, npairs);
        if (!form->fields) {
            Free(form);
            return NULL;
        }
    }
    for (i = 0, start = 0; i <= len; i++) {
        size_t eq, nlen, vlen;
        char *name, *value;
        if (i < len && s[i] != '&')
            continue;
        if (i == start) {
            start = i + 1;
            continue;
        }
        eq = start;
        while (eq < i && s[eq] != '=')
            eq++;
        name = _form_decode_dup(s + start, eq - start, dflags, &nlen);
        value = eq < i ? _form_decode_dup(s + eq + 1, i - eq - 1, dflags, &vlen)
                       : _form_decode_dup("", 0, dflags, &vlen);
        start = i + 1;
        if (!name || !value || nlen == 0) {
            FreeNoLog(name);
            FreeNoLog(value);
            if (flags & N_HTTP_FORM_SKIP_INVALID)
                continue;
            n_http_form_free(&form);
            return NULL;
        }
        form->fields[form->nb_fields].name = name;
        form->fields[form->nb_fields].name_len = nlen;
        form->fields[form->nb_fields].value = value;
        form->fields[form->nb_fields].value_len = vlen;
        form->nb_fields++;
    }
    return form;
}

const char* n_http_form_get(const N_HTTP_FORM* form, const char* name, size_t* value_len) {
    size_t i;
    if (value_len)
        *value_len = 0;
    if (!form || !name)
        return NULL;
    for (i = form->nb_fields; i > 0; i--) {
        const N_HTTP_FIELD* f = &form->fields[i - 1];
        if (strcmp(f->name, name) == 0) {
            if (value_len)
                *value_len = f->value_len;
            return f->value;
        }
    }
    return NULL;
}

size_t n_http_form_count(const N_HTTP_FORM* form, const char* name) {
    size_t i, count = 0;
    if (!form || !name)
        return 0;
    for (i = 0; i < form->nb_fields; i++)
        if (strcmp(form->fields[i].name, name) == 0)
            count++;
    return count;
}

/* Multipart/form-data, strict and zero-copy. */

/* most bytes of preamble accepted before the first delimiter */
#define _MP_PREAMBLE_MAX 1024

static int _mp_lc(int c) {
    return (c >= 'A' && c <= 'Z') ? c - 'A' + 'a' : c;
}

/* ASCII case-insensitive compare of n bytes against a lowercase literal. */
static int _mp_ieq(const char* s, size_t n, const char* lit) {
    size_t i;
    if (strlen(lit) != n)
        return 0;
    for (i = 0; i < n; i++)
        if (_mp_lc((unsigned char)s[i]) != lit[i])
            return 0;
    return 1;
}

static int _mp_tchar(unsigned char c) {
    if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'))
        return 1;
    return c != '\0' && strchr("!#$%&'*+-.^_`|~", c) != NULL;
}

/* An RFC 2046 bchar (space allowed, not as the last character). */
static int _mp_bchar(unsigned char c) {
    if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'))
        return 1;
    return c != '\0' && strchr("'()+_,-./:=? ", c) != NULL;
}

/* One parameter of a header value: name and (unquoted) value. */
typedef struct {
    const char* name;
    size_t name_len;
    char value[N_HTTP_FORM_PART_FILENAME_MAX + 2];
    size_t value_len;
} _mp_param;

/* Parse the next "; name=value" parameter at *pos of [v, v+n). Returns 1 with
 * p filled, 0 at the end, -1 when malformed (or a value over the cap). A
 * quoted value takes backslash escapes. */
static int _mp_next_param(const char* v, size_t n, size_t* pos, _mp_param* p) {
    size_t i = *pos, ns;
    while (i < n && (v[i] == ' ' || v[i] == '\t'))
        i++;
    if (i == n)
        return 0;
    if (v[i] != ';')
        return -1;
    i++;
    while (i < n && (v[i] == ' ' || v[i] == '\t'))
        i++;
    ns = i;
    while (i < n && _mp_tchar((unsigned char)v[i]))
        i++;
    if (i == ns || i == n || v[i] != '=')
        return -1;
    p->name = v + ns;
    p->name_len = i - ns;
    i++;
    p->value_len = 0;
    if (i < n && v[i] == '"') {
        i++;
        for (;;) {
            char c;
            if (i >= n)
                return -1; /* unterminated quoted string */
            c = v[i++];
            if (c == '"')
                break;
            if (c == '\\') {
                if (i >= n)
                    return -1;
                c = v[i++];
            }
            if (p->value_len >= sizeof(p->value) - 1)
                return -1;
            p->value[p->value_len++] = c;
        }
    } else {
        while (i < n && v[i] != ';' && v[i] != ' ' && v[i] != '\t') {
            if (!_mp_tchar((unsigned char)v[i]) || p->value_len >= sizeof(p->value) - 1)
                return -1;
            p->value[p->value_len++] = v[i++];
        }
        if (p->value_len == 0)
            return -1;
    }
    p->value[p->value_len] = '\0';
    *pos = i;
    return 1;
}

int n_http_form_multipart_boundary(const char* content_type, size_t len, char* out) {
    size_t i = 0, ts, pos;
    int found = 0, r;
    _mp_param p;
    if (!content_type || !out)
        return FALSE;
    out[0] = '\0';
    while (i < len && (content_type[i] == ' ' || content_type[i] == '\t'))
        i++;
    ts = i;
    while (i < len && content_type[i] != ';' && content_type[i] != ' ' && content_type[i] != '\t')
        i++;
    if (!_mp_ieq(content_type + ts, i - ts, "multipart/form-data"))
        return FALSE;
    pos = i;
    while ((r = _mp_next_param(content_type, len, &pos, &p)) == 1) {
        if (!_mp_ieq(p.name, p.name_len, "boundary"))
            continue;
        if (found || p.value_len == 0 || p.value_len > N_HTTP_FORM_BOUNDARY_MAX || p.value[p.value_len - 1] == ' ')
            return FALSE;
        for (i = 0; i < p.value_len; i++)
            if (!_mp_bchar((unsigned char)p.value[i]))
                return FALSE;
        memcpy(out, p.value, p.value_len + 1);
        found = 1;
    }
    if (r < 0 || !found) {
        out[0] = '\0';
        return FALSE;
    }
    return TRUE;
}

/* Text fit for a name or filename: UTF-8, no control byte. */
static int _mp_text_ok(const char* s, size_t n) {
    size_t i;
    for (i = 0; i < n; i++) {
        unsigned char c = (unsigned char)s[i];
        if (c < 0x20 || c == 0x7f)
            return 0;
    }
    return n_str_utf8_valid(s, n);
}

/* First offset of the delimiter "\r\n--boundary" followed by "--", optional
 * padding then CRLF, at or after from; *after receives the offset past the
 * delimiter line (or past "--" for the closing one), *closing 1 for it.
 * Returns the offset, or (size_t)-1 when there is none. */
static size_t _mp_find_delim(const char* b, size_t len, size_t from, const char* boundary, size_t blen, size_t* after, int* closing) {
    size_t i;
    for (i = from; i + 4 + blen <= len; i++) {
        size_t k;
        if (b[i] != '\r' || b[i + 1] != '\n' || b[i + 2] != '-' || b[i + 3] != '-' || memcmp(b + i + 4, boundary, blen) != 0)
            continue;
        k = i + 4 + blen;
        if (k + 2 <= len && b[k] == '-' && b[k + 1] == '-') {
            *after = k + 2;
            *closing = 1;
            return i;
        }
        while (k < len && (b[k] == ' ' || b[k] == '\t'))
            k++;
        if (k + 2 <= len && b[k] == '\r' && b[k + 1] == '\n') {
            *after = k + 2;
            *closing = 0;
            return i;
        }
        /* "--boundary" followed by anything else is body data */
    }
    return (size_t)-1;
}

/* Parse the header block [h, h+n) of one part into part. */
static int _mp_part_headers(const char* h, size_t n, N_HTTP_FORM_PART* part) {
    size_t i = 0;
    int have_disp = 0, have_type = 0;
    while (i < n) {
        size_t le = i, colon, vs, ve;
        while (le + 1 < n && !(h[le] == '\r' && h[le + 1] == '\n'))
            le++;
        if (le + 1 >= n)
            le = n; /* last line, without its CRLF (the block ends before CRLFCRLF) */
        colon = i;
        while (colon < le && h[colon] != ':')
            colon++;
        if (colon == i || colon >= le)
            return FALSE;
        {
            size_t k;
            for (k = i; k < colon; k++)
                if (!_mp_tchar((unsigned char)h[k]))
                    return FALSE; /* also refuses obs-fold continuation lines */
        }
        vs = colon + 1;
        while (vs < le && (h[vs] == ' ' || h[vs] == '\t'))
            vs++;
        ve = le;
        while (ve > vs && (h[ve - 1] == ' ' || h[ve - 1] == '\t'))
            ve--;
        {
            size_t k;
            for (k = vs; k < ve; k++)
                if (((unsigned char)h[k] < 0x20 && h[k] != '\t') || h[k] == 0x7f)
                    return FALSE;
        }
        if (_mp_ieq(h + i, colon - i, "content-disposition")) {
            size_t pos = vs, ts = vs;
            int r, have_name = 0;
            _mp_param p;
            if (have_disp)
                return FALSE;
            have_disp = 1;
            while (pos < ve && h[pos] != ';' && h[pos] != ' ' && h[pos] != '\t')
                pos++;
            if (!_mp_ieq(h + ts, pos - ts, "form-data"))
                return FALSE;
            while ((r = _mp_next_param(h, ve, &pos, &p)) == 1) {
                if (_mp_ieq(p.name, p.name_len, "name")) {
                    if (have_name || p.value_len == 0 || p.value_len > N_HTTP_FORM_PART_NAME_MAX || !_mp_text_ok(p.value, p.value_len))
                        return FALSE;
                    memcpy(part->name, p.value, p.value_len + 1);
                    have_name = 1;
                } else if (_mp_ieq(p.name, p.name_len, "filename")) {
                    if (part->has_filename || p.value_len > N_HTTP_FORM_PART_FILENAME_MAX || !_mp_text_ok(p.value, p.value_len))
                        return FALSE;
                    memcpy(part->filename, p.value, p.value_len + 1);
                    part->has_filename = 1;
                } else if (_mp_ieq(p.name, p.name_len, "filename*")) {
                    return FALSE; /* browsers never send it for form-data; refuse the ambiguity */
                }
            }
            if (r < 0 || !have_name)
                return FALSE;
        } else if (_mp_ieq(h + i, colon - i, "content-type")) {
            if (have_type || ve - vs > N_HTTP_FORM_PART_TYPE_MAX)
                return FALSE;
            have_type = 1;
            memcpy(part->content_type, h + vs, ve - vs);
            part->content_type[ve - vs] = '\0';
        }
        i = le + 2;
    }
    return have_disp;
}

N_HTTP_FORM_MULTIPART* n_http_form_multipart_parse(const char* buf, size_t len, const char* boundary, size_t max_parts) {
    N_HTTP_FORM_MULTIPART* mp = NULL;
    size_t blen, cursor, cap = 8, after = 0;
    int closing = 0;

    if (!buf || !boundary || max_parts == 0)
        return NULL;
    blen = strlen(boundary);
    if (blen == 0 || blen > N_HTTP_FORM_BOUNDARY_MAX)
        return NULL;
    /* the first delimiter: at the very start, or after a short preamble */
    if (len >= 2 + blen && buf[0] == '-' && buf[1] == '-' && memcmp(buf + 2, boundary, blen) == 0) {
        size_t k = 2 + blen;
        while (k < len && (buf[k] == ' ' || buf[k] == '\t'))
            k++;
        if (k + 2 > len || buf[k] != '\r' || buf[k + 1] != '\n')
            return NULL;
        cursor = k + 2;
    } else {
        size_t d = _mp_find_delim(buf, len < _MP_PREAMBLE_MAX + 4 + blen + 2 ? len : _MP_PREAMBLE_MAX + 4 + blen + 2, 0, boundary, blen, &after, &closing);
        if (d == (size_t)-1 || closing)
            return NULL;
        cursor = after;
    }
    Malloc(mp, N_HTTP_FORM_MULTIPART, 1);
    __n_assert(mp, return NULL);
    Malloc(mp->parts, N_HTTP_FORM_PART, cap);
    if (!mp->parts) {
        Free(mp);
        return NULL;
    }
    for (;;) {
        N_HTTP_FORM_PART* part;
        size_t hend, d, scan;
        int head_found = 0;
        /* fewer than four bytes left cannot hold the CRLFCRLF a header
         * block ends with: refused before the scan, which then always has
         * room to look */
        if (cursor > len || len - cursor < 4)
            goto refuse;
        /* the part's header block ends at the first CRLFCRLF; the bound is
         * capped by an if rather than a conditional expression, whose
         * subtraction cppcheck 2.10 misreads as always below 1 */
        scan = len - cursor;
        if (scan > N_HTTP_FORM_PART_HEAD_MAX + 4)
            scan = N_HTTP_FORM_PART_HEAD_MAX + 4;
        for (hend = 0; hend + 4 <= scan; hend++) {
            if (buf[cursor + hend] == '\r' && buf[cursor + hend + 1] == '\n' && buf[cursor + hend + 2] == '\r' && buf[cursor + hend + 3] == '\n') {
                head_found = 1;
                break;
            }
        }
        if (!head_found)
            goto refuse; /* no blank line within the cap */
        if (mp->nb_parts >= max_parts)
            goto refuse;
        if (mp->nb_parts == cap) {
            N_HTTP_FORM_PART* grown = NULL;
            size_t ncap = cap * 2 < max_parts ? cap * 2 : max_parts;
            Malloc(grown, N_HTTP_FORM_PART, ncap);
            if (!grown)
                goto refuse;
            memcpy(grown, mp->parts, cap * sizeof(*grown));
            Free(mp->parts);
            mp->parts = grown;
            cap = ncap;
        }
        part = &mp->parts[mp->nb_parts];
        memset(part, 0, sizeof(*part));
        if (!_mp_part_headers(buf + cursor, hend, part))
            goto refuse;
        part->body_off = cursor + hend + 4;
        /* the body runs to the next delimiter; an empty body has the
         * delimiter's CRLF right after the blank line */
        d = _mp_find_delim(buf, len, part->body_off - 2, boundary, blen, &after, &closing);
        if (d == (size_t)-1 || d + 2 < part->body_off)
            goto refuse; /* truncated: no delimiter after the part */
        part->body_len = d + 2 == part->body_off ? 0 : d - part->body_off;
        mp->nb_parts++;
        if (closing)
            return mp; /* the epilogue, if any, is ignored */
        cursor = after;
    }
refuse:
    n_http_form_multipart_free(&mp);
    return NULL;
}

const N_HTTP_FORM_PART* n_http_form_multipart_get(const N_HTTP_FORM_MULTIPART* mp, const char* name) {
    size_t i;
    if (!mp || !name)
        return NULL;
    for (i = mp->nb_parts; i > 0; i--)
        if (strcmp(mp->parts[i - 1].name, name) == 0)
            return &mp->parts[i - 1];
    return NULL;
}

size_t n_http_form_multipart_count(const N_HTTP_FORM_MULTIPART* mp, const char* name) {
    size_t i, n = 0;
    if (!mp || !name)
        return 0;
    for (i = 0; i < mp->nb_parts; i++)
        if (strcmp(mp->parts[i].name, name) == 0)
            n++;
    return n;
}

void n_http_form_multipart_free(N_HTTP_FORM_MULTIPART** mp) {
    if (!mp || !*mp)
        return;
    FreeNoLog((*mp)->parts);
    FreeNoLog(*mp);
}