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

examples/ex_http_form.c

Functions

Name
void test_multipart(void )
multipart/form-data
void test_form(void )
query strings and urlencoded bodies
void test_decode(void )
percent-decoding
int process_args(int argc, char ** argv)
read -V
int part_body_is(const char * buf, const N_HTTP_FORM_PART * p, const char * want)
body of a part as a string comparison
N_HTTP_FORM_MULTIPART * mp_parse(const char * body, size_t max)
parse a NUL-terminated multipart body
int main(int argc, char ** argv)
run the checks
void expect_true(const char * what, int ok)
count a failed check
int decodes_to(const char * in, int flags, const char * want)
decode a NUL-terminated literal and compare with the expected result

Attributes

Name
int log_level
log level from -V
int failures
failed checks

Defines

Name
BND(ct)
boundary of a NUL-terminated Content-Type literal, into b

Functions Documentation

function test_multipart

static void test_multipart(
    void 
)

multipart/form-data

function test_form

static void test_form(
    void 
)

query strings and urlencoded bodies

function test_decode

static void test_decode(
    void 
)

percent-decoding

function process_args

static int process_args(
    int argc,
    char ** argv
)

read -V

Parameters:

  • argc argument count
  • argv arguments

Return: 0, or -1 on a bad argument

function part_body_is

static int part_body_is(
    const char * buf,
    const N_HTTP_FORM_PART * p,
    const char * want
)

body of a part as a string comparison

Parameters:

  • buf the parsed buffer
  • p the part
  • want expected body

Return: non-zero when equal

function mp_parse

static N_HTTP_FORM_MULTIPART * mp_parse(
    const char * body,
    size_t max
)

parse a NUL-terminated multipart body

Parameters:

  • body the body
  • max most parts

Return: the parse result

function main

int main(
    int argc,
    char ** argv
)

run the checks

Parameters:

  • argc argument count
  • argv arguments

Return: 0 when every check passes

function expect_true

static void expect_true(
    const char * what,
    int ok
)

count a failed check

Parameters:

  • what the check
  • ok its result

function decodes_to

static int decodes_to(
    const char * in,
    int flags,
    const char * want
)

decode a NUL-terminated literal and compare with the expected result

Parameters:

  • in encoded text
  • flags decode flags
  • want expected decoded text, or NULL when decoding must fail

Return: non-zero when the outcome is the expected one

Attributes Documentation

variable log_level

static int log_level = 3;

log level from -V

variable failures

static int failures = 0;

failed checks

Macros Documentation

define BND

#define BND(
    ct
)
n_http_form_multipart_boundary((ct), strlen(ct), b)

boundary of a NUL-terminated Content-Type literal, into b

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

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

static int log_level = LOG_ERR;
static int failures = 0;

static void expect_true(const char* what, int ok) {
    if (!ok) {
        n_log(LOG_ERR, "FAIL: %s", what);
        failures++;
    } else {
        n_log(LOG_INFO, "ok: %s", what);
    }
}

static int process_args(int argc, char** argv) {
    int opt = 0;

    while ((opt = getopt(argc, argv, "V:")) != -1) {
        if (opt == 'V') {
            log_level = atoi(optarg);
        } else {
            return -1;
        }
    }
    return 0;
}

static int decodes_to(const char* in, int flags, const char* want) {
    N_STR* out = NULL;
    int ok = n_http_percent_decode(in, strlen(in), flags, &out);
    int res;
    if (!want)
        res = (ok == FALSE);
    else
        res = ok == TRUE && out && out->written == strlen(want) && memcmp(out->data, want, out->written) == 0;
    free_nstr(&out);
    return res;
}

static void test_decode(void) {
    N_STR* out = NULL;

    expect_true("plain text unchanged", decodes_to("abc", 0, "abc"));
    expect_true("escapes decoded, any hex case", decodes_to("%2Fa%2fb%20c", 0, "/a/b c"));
    expect_true("UTF-8 escapes decoded", decodes_to("caf%C3%A9", 0, "caf\xc3\xa9"));
    expect_true("'+' kept without PLUS_SPACE", decodes_to("a+b", 0, "a+b"));
    expect_true("'+' is a space with PLUS_SPACE", decodes_to("a+b", N_HTTP_FORM_PLUS_SPACE, "a b"));
    expect_true("%2B stays '+' with PLUS_SPACE", decodes_to("a%2Bb", N_HTTP_FORM_PLUS_SPACE, "a+b"));
    expect_true("empty input", decodes_to("", 0, ""));
    expect_true("lone '%' refused", decodes_to("a%", 0, NULL));
    expect_true("one hex digit refused", decodes_to("a%4", 0, NULL));
    expect_true("non-hex escape refused", decodes_to("a%zz", 0, NULL));
    expect_true("decoded NUL refused", decodes_to("a%00b", 0, NULL));
    expect_true("decoded NUL refused even with RAW_BYTES", decodes_to("a%00b", N_HTTP_FORM_RAW_BYTES, NULL));
    expect_true("ill-formed UTF-8 refused", decodes_to("%E9t%E9", 0, NULL));
    expect_true("overlong '/' refused", decodes_to("%C0%AF", 0, NULL));
    expect_true("ill-formed UTF-8 kept with RAW_BYTES", decodes_to("%E9", N_HTTP_FORM_RAW_BYTES, "\xe9"));

    /* appends; a failure leaves the destination as it was */
    out = char_to_nstr("x=");
    expect_true("append", n_http_percent_decode("%41", 3, 0, &out) == TRUE && strcmp(out->data, "x=A") == 0);
    expect_true("failed append returns FALSE", n_http_percent_decode("%4", 2, 0, &out) == FALSE);
    expect_true("failed append leaves the destination", out->written == 3 && strcmp(out->data, "x=A") == 0);
    free_nstr(&out);
    expect_true("NULL destination refused", n_http_percent_decode("a", 1, 0, NULL) == FALSE);
    expect_true("NULL source with a length refused", n_http_percent_decode(NULL, 1, 0, &out) == FALSE && out == NULL);
}

static void test_form(void) {
    N_HTTP_FORM* f = NULL;
    size_t vlen = 0;

    f = n_http_form_parse("s=hello+world&page=2&&tag%5B%5D=a&tag%5B%5D=b&flag&e=&s=last", 60, 50, 0);
    expect_true("query parses", f != NULL);
    if (f) {
        const char* v = NULL;
        expect_true("empty pair skipped: 7 fields", f->nb_fields == 7);
        expect_true("fields keep their order", strcmp(f->fields[0].name, "s") == 0 && strcmp(f->fields[1].name, "page") == 0);
        v = n_http_form_get(f, "s", &vlen);
        expect_true("last occurrence wins, like PHP", v && strcmp(v, "last") == 0 && vlen == 4);
        expect_true("'+' decoded as a space in values", strcmp(f->fields[0].value, "hello world") == 0);
        expect_true("bracket names decoded and counted", n_http_form_count(f, "tag[]") == 2);
        v = n_http_form_get(f, "flag", &vlen);
        expect_true("pair without '=' has an empty value", v && v[0] == '\0' && vlen == 0);
        v = n_http_form_get(f, "e", &vlen);
        expect_true("'name=' has an empty value", v && v[0] == '\0' && vlen == 0);
        expect_true("absent field is NULL", n_http_form_get(f, "nope", &vlen) == NULL && vlen == 0);
        expect_true("names are case-sensitive", n_http_form_get(f, "S", NULL) == NULL);
        n_http_form_free(&f);
        expect_true("free sets NULL", f == NULL);
    }

    f = n_http_form_parse("a=1&b=%zz&c=3", 13, 10, 0);
    expect_true("bad escape fails the whole form", f == NULL);
    f = n_http_form_parse("a=1&b=%zz&c=%00&d=%E9&=x&e=5", 28, 10, N_HTTP_FORM_SKIP_INVALID);
    expect_true("SKIP_INVALID parses", f != NULL);
    if (f) {
        expect_true("SKIP_INVALID drops the bad pairs", f->nb_fields == 2 && n_http_form_get(f, "a", NULL) && n_http_form_get(f, "e", NULL));
        n_http_form_free(&f);
    }
    f = n_http_form_parse("=x", 2, 10, 0);
    expect_true("empty name refused", f == NULL);
    f = n_http_form_parse("a=1&b=2&c=3", 11, 2, N_HTTP_FORM_SKIP_INVALID);
    expect_true("more pairs than max_fields refused, even with SKIP_INVALID", f == NULL);
    f = n_http_form_parse("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&a=1", 35, 1, 0);
    expect_true("empty pairs do not count against max_fields", f != NULL && f->nb_fields == 1);
    n_http_form_free(&f);
    f = n_http_form_parse("v=%E9", 5, 4, N_HTTP_FORM_RAW_BYTES);
    expect_true("RAW_BYTES keeps a latin-1 byte", f != NULL && f->fields[0].value_len == 1 && (unsigned char)f->fields[0].value[0] == 0xE9);
    n_http_form_free(&f);
    f = n_http_form_parse("", 0, 4, 0);
    expect_true("empty query is an empty form", f != NULL && f->nb_fields == 0 && n_http_form_get(f, "a", NULL) == NULL);
    n_http_form_free(&f);
    f = n_http_form_parse(NULL, 0, 4, 0);
    expect_true("NULL with length 0 is an empty form", f != NULL && f->nb_fields == 0);
    n_http_form_free(&f);
    expect_true("NULL with a length refused", n_http_form_parse(NULL, 3, 4, 0) == NULL);
    expect_true("max_fields 0 refused", n_http_form_parse("a=1", 3, 0, 0) == NULL);
    expect_true("count on NULL", n_http_form_count(NULL, "a") == 0);
    n_http_form_free(NULL);
    n_http_form_free(&f);
}

static int part_body_is(const char* buf, const N_HTTP_FORM_PART* p, const char* want) {
    return p && p->body_len == strlen(want) && memcmp(buf + p->body_off, want, p->body_len) == 0;
}

static N_HTTP_FORM_MULTIPART* mp_parse(const char* body, size_t max) {
    return n_http_form_multipart_parse(body, strlen(body), "XyZ", max);
}

#define BND(ct) n_http_form_multipart_boundary((ct), strlen(ct), b)

static void test_multipart(void) {
    char b[N_HTTP_FORM_BOUNDARY_MAX + 1];
    N_HTTP_FORM_MULTIPART* mp;
    static const char ok_body[] =
        "--XyZ\r\n"
        "Content-Disposition: form-data; name=\"title\"\r\n\r\n"
        "Hello\r\n"
        "--XyZ\r\n"
        "Content-Disposition: form-data; name=\"file\"; filename=\"a \\\"q\\\".png\"\r\n"
        "Content-Type: image/png\r\n\r\n"
        "\x89PNG\r\n--XyZnot-a-delimiter\r\n\r\n\x00\x01\r\n"
        "--XyZ\r\n"
        "Content-Disposition: form-data; name=\"empty\"\r\n\r\n"
        "\r\n"
        "--XyZ\r\n"
        "content-disposition: FORM-DATA; filename=\"\"; name=\"tag[]\"\r\n\r\n"
        "x\r\n"
        "--XyZ\r\n"
        "Content-Disposition: form-data; name=\"title\"\r\n\r\n"
        "Second\r\n"
        "--XyZ--\r\nepilogue ignored";
    const size_t ok_len = sizeof(ok_body) - 1; /* the file part holds a NUL */

    /* the boundary from Content-Type */
    expect_true("boundary token", BND("multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxk") &&
                                      strcmp(b, "----WebKitFormBoundary7MA4YWxk") == 0);
    expect_true("boundary quoted, any case", BND("Multipart/Form-Data; charset=utf-8; BOUNDARY=\"a b:c\"") &&
                                                 strcmp(b, "a b:c") == 0);
    expect_true("boundary inside another quoted parameter not taken",
                BND("multipart/form-data; x=\"boundary=evil\"; boundary=real") && strcmp(b, "real") == 0);
    expect_true("two boundaries refused", !BND("multipart/form-data; boundary=a; boundary=b"));
    expect_true("other media type refused", !BND("multipart/mixed; boundary=a"));
    expect_true("no boundary refused", !BND("multipart/form-data"));
    expect_true("boundary over 70 refused",
                !BND("multipart/form-data; boundary=12345678901234567890123456789012345678901234567890123456789012345678901"));
    expect_true("boundary with a bad character refused", !BND("multipart/form-data; boundary=\"a<b\""));
    expect_true("boundary ending in a space refused", !BND("multipart/form-data; boundary=\"ab \""));
    expect_true("unterminated quote refused", !BND("multipart/form-data; boundary=\"ab"));

    mp = n_http_form_multipart_parse(ok_body, ok_len, "XyZ", 10);
    expect_true("browser body parses", mp && mp->nb_parts == 5);
    if (mp) {
        const N_HTTP_FORM_PART* f = n_http_form_multipart_get(mp, "file");
        expect_true("first field", strcmp(mp->parts[0].name, "title") == 0 && !mp->parts[0].has_filename && part_body_is(ok_body, &mp->parts[0], "Hello"));
        expect_true("file part: escaped quotes in filename", f && f->has_filename && strcmp(f->filename, "a \"q\".png") == 0 && strcmp(f->content_type, "image/png") == 0);
        expect_true("file body binary-safe, look-alike delimiter kept", f && f->body_len == 4 + 2 + 20 + 4 + 2 && memcmp(ok_body + f->body_off, "\x89PNG\r\n--XyZnot-a-delimiter", 26) == 0);
        expect_true("empty body", part_body_is(ok_body, n_http_form_multipart_get(mp, "empty"), ""));
        expect_true("header names and form-data any case; filename=\"\" is a file field",
                    strcmp(mp->parts[3].name, "tag[]") == 0 && mp->parts[3].has_filename && mp->parts[3].filename[0] == '\0');
        expect_true("last occurrence wins, count", part_body_is(ok_body, n_http_form_multipart_get(mp, "title"), "Second") && n_http_form_multipart_count(mp, "title") == 2);
        expect_true("absent", n_http_form_multipart_get(mp, "nope") == NULL && n_http_form_multipart_count(mp, "nope") == 0);
        n_http_form_multipart_free(&mp);
        expect_true("free sets NULL", mp == NULL);
    }

    /* the parameter confusion the old parser fell for */
    mp = mp_parse("--XyZ\r\nContent-Disposition: form-data; filename=\"evil\"; name=\"real\"\r\n\r\nv\r\n--XyZ--", 4);
    expect_true("name= inside filename= is not the name", mp && strcmp(mp->parts[0].name, "real") == 0 && strcmp(mp->parts[0].filename, "evil") == 0);
    n_http_form_multipart_free(&mp);
    expect_true("two name= refused", mp_parse("--XyZ\r\nContent-Disposition: form-data; name=\"a\"; name=\"b\"\r\n\r\nv\r\n--XyZ--", 4) == NULL);
    expect_true("two filename= refused", mp_parse("--XyZ\r\nContent-Disposition: form-data; name=\"a\"; filename=\"x\"; filename=\"y\"\r\n\r\nv\r\n--XyZ--", 4) == NULL);
    expect_true("filename* refused", mp_parse("--XyZ\r\nContent-Disposition: form-data; name=\"a\"; filename*=UTF-8''x\r\n\r\nv\r\n--XyZ--", 4) == NULL);
    expect_true("two Content-Disposition refused",
                mp_parse("--XyZ\r\nContent-Disposition: form-data; name=\"a\"\r\nContent-Disposition: form-data; name=\"b\"\r\n\r\nv\r\n--XyZ--", 4) == NULL);
    expect_true("no Content-Disposition refused", mp_parse("--XyZ\r\nContent-Type: text/plain\r\n\r\nv\r\n--XyZ--", 4) == NULL);
    expect_true("not form-data refused", mp_parse("--XyZ\r\nContent-Disposition: attachment; name=\"a\"\r\n\r\nv\r\n--XyZ--", 4) == NULL);
    expect_true("empty name refused", mp_parse("--XyZ\r\nContent-Disposition: form-data; name=\"\"\r\n\r\nv\r\n--XyZ--", 4) == NULL);
    expect_true("CR in a name refused", mp_parse("--XyZ\r\nContent-Disposition: form-data; name=\"a\rb\"\r\n\r\nv\r\n--XyZ--", 4) == NULL);
    expect_true("ill-formed UTF-8 filename refused", mp_parse("--XyZ\r\nContent-Disposition: form-data; name=\"a\"; filename=\"\xe9.png\"\r\n\r\nv\r\n--XyZ--", 4) == NULL);
    expect_true("folded header refused", mp_parse("--XyZ\r\nContent-Disposition: form-data;\r\n name=\"a\"\r\n\r\nv\r\n--XyZ--", 4) == NULL);
    expect_true("header line without colon refused", mp_parse("--XyZ\r\nContent-Disposition: form-data; name=\"a\"\r\nbogus\r\n\r\nv\r\n--XyZ--", 4) == NULL);
    /* truncation and limits */
    expect_true("missing closing delimiter (truncated upload) refused", mp_parse("--XyZ\r\nContent-Disposition: form-data; name=\"a\"\r\n\r\nvalue", 4) == NULL);
    expect_true("no blank line refused", mp_parse("--XyZ\r\nContent-Disposition: form-data; name=\"a\"\r\n", 4) == NULL);
    expect_true("more parts than max refused",
                mp_parse("--XyZ\r\nContent-Disposition: form-data; name=\"a\"\r\n\r\n1\r\n--XyZ\r\nContent-Disposition: form-data; name=\"b\"\r\n\r\n2\r\n--XyZ--", 1) == NULL);
    {
        static char big[N_HTTP_FORM_PART_HEAD_MAX + 200];
        size_t o = (size_t)snprintf(big, sizeof(big), "--XyZ\r\nContent-Disposition: form-data; name=\"a\"\r\nX-Pad: ");
        memset(big + o, 'p', N_HTTP_FORM_PART_HEAD_MAX);
        o += N_HTTP_FORM_PART_HEAD_MAX;
        memcpy(big + o, "\r\n\r\nv\r\n--XyZ--", 15);
        expect_true("part header block over the cap refused", n_http_form_multipart_parse(big, o + 14, "XyZ", 4) == NULL);
    }
    {
        /* a header block of exactly the cap, its CRLFCRLF in the last four
         * bytes looked at: accepted */
        static char edge[N_HTTP_FORM_PART_HEAD_MAX + 200];
        const char* lead = "--XyZ\r\n";
        size_t start = strlen(lead);
        size_t o = (size_t)snprintf(edge, sizeof(edge), "%sContent-Disposition: form-data; name=\"a\"\r\nX-Pad: ", lead);
        N_HTTP_FORM_MULTIPART* at_cap = NULL;
        memset(edge + o, 'p', start + N_HTTP_FORM_PART_HEAD_MAX - o);
        o = start + N_HTTP_FORM_PART_HEAD_MAX;
        memcpy(edge + o, "\r\n\r\nv\r\n--XyZ--", 15);
        at_cap = n_http_form_multipart_parse(edge, o + 14, "XyZ", 4);
        expect_true("part header block of exactly the cap accepted", at_cap && at_cap->nb_parts == 1 && part_body_is(edge, &at_cap->parts[0], "v"));
        n_http_form_multipart_free(&at_cap);
    }
    {
        char longname[300];
        static char body[600];
        memset(longname, 'n', N_HTTP_FORM_PART_NAME_MAX + 1);
        longname[N_HTTP_FORM_PART_NAME_MAX + 1] = '\0';
        snprintf(body, sizeof(body), "--XyZ\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\nv\r\n--XyZ--", longname);
        expect_true("name over the cap refused", mp_parse(body, 4) == NULL);
    }
    /* framing details */
    mp = mp_parse("preamble text\r\n--XyZ  \r\nContent-Disposition: form-data; name=\"a\"\r\n\r\nv\r\n--XyZ\t\r\nContent-Disposition: form-data; name=b\r\n\r\nw\r\n--XyZ--", 4);
    expect_true("preamble, transport padding, token name", mp && mp->nb_parts == 2 && strcmp(mp->parts[1].name, "b") == 0);
    n_http_form_multipart_free(&mp);
    expect_true("delimiter not at the start and no CRLF before it refused", mp_parse("junk--XyZ\r\nContent-Disposition: form-data; name=\"a\"\r\n\r\nv\r\n--XyZ--", 4) == NULL);
    expect_true("closing delimiter first refused", mp_parse("--XyZ--", 4) == NULL);
    expect_true("wrong boundary refused", n_http_form_multipart_parse("--AbC\r\n", 7, "XyZ", 4) == NULL);
    expect_true("NULL / zero max refused", n_http_form_multipart_parse(NULL, 0, "XyZ", 4) == NULL && n_http_form_multipart_parse("--XyZ--", 7, "XyZ", 0) == NULL);
    n_http_form_multipart_free(NULL);
}
#undef BND

int main(int argc, char** argv) {
    if (process_args(argc, argv) != 0) {
        fprintf(stderr, "usage: %s [-V LOG_LEVEL]\n", argv[0]);
        return 1;
    }
    set_log_level(log_level);

    test_decode();
    test_form();
    test_multipart();

    if (failures) {
        n_log(LOG_ERR, "ex_http_form: %d failure(s)", failures);
        return 1;
    }
    n_log(LOG_NOTICE, "ex_http_form: all checks passed");
    return 0;
}