/*
 * Copyright (c) 2012 Igor Afonov afonov@gmail.com
 *
 * SPDX-License-Identifier: MIT
 */
#include "multipart_parser.h"
#include <stdio.h>
#include <stdarg.h>
#include <string.h>

static void multipart_log(const char __attribute__((__unused__)) * format, ...)
{
#ifdef DEBUG_MULTIPART
	va_list args;
	va_start(args, format);

	fprintf(stderr, "[HTTP_MULTIPART_PARSER] %s:%d: ", __FILE__, __LINE__);
	vfprintf(stderr, format, args);
	fprintf(stderr, "\n");
	va_end(args);
#endif
}

#define NOTIFY_CB(FOR)                                                 \
do {                                                                   \
  if (p->settings->on_##FOR) {                                         \
    if (p->settings->on_##FOR(p) != 0) {                               \
      return i;                                                        \
    }                                                                  \
  }                                                                    \
} while (0)

#define EMIT_DATA_CB(FOR, ptr, len)                                    \
do {                                                                   \
  if (p->settings->on_##FOR) {                                         \
    if (p->settings->on_##FOR(p, ptr, len) != 0) {                     \
      return i;                                                        \
    }                                                                  \
  }                                                                    \
} while (0)

#define LF 10
#define CR 13

struct multipart_parser {
	void *data;

	size_t index;
	size_t boundary_length;

	unsigned char state;

	const multipart_parser_settings *settings;

	char *lookbehind;
	char multipart_boundary[1];
};

enum state {
	s_uninitialized = 1,
	s_start,
	s_start_boundary,
	s_header_field_start,
	s_header_field,
	s_headers_almost_done,
	s_header_value_start,
	s_header_value,
	s_header_value_almost_done,
	s_part_data_start,
	s_part_data,
	s_part_data_almost_boundary,
	s_part_data_boundary,
	s_part_data_almost_end,
	s_part_data_end,
	s_part_data_final_hyphen,
	s_end
};

multipart_parser *multipart_parser_init
    (const char *boundary, const multipart_parser_settings * settings) {

	multipart_parser *p = malloc(sizeof(multipart_parser) +
				     strlen(boundary) + strlen(boundary) + 9);

	if (!p)
		return NULL;
	strcpy(p->multipart_boundary, boundary);
	p->boundary_length = strlen(boundary);

	p->lookbehind = (p->multipart_boundary + p->boundary_length + 1);

	p->index = 0;
	p->state = s_start;
	p->settings = settings;

	return p;
}

void multipart_parser_free(multipart_parser * p)
{
	free(p);
}

void multipart_parser_set_data(multipart_parser * p, void *data)
{
	p->data = data;
}

void *multipart_parser_get_data(multipart_parser * p)
{
	return p->data;
}

size_t multipart_parser_execute(multipart_parser * p, const char *buf,
				size_t len)
{
	size_t i = 0;
	size_t mark = 0;
	char c, cl;
	int is_last = 0;

	while (i < len) {
		c = buf[i];
		is_last = (i == (len - 1));
		switch (p->state) {
		case s_start:
			multipart_log("s_start");
			p->index = 0;
			p->state = s_start_boundary;

			/* fallthrough */
		case s_start_boundary:
			multipart_log("s_start_boundary");
			if (p->index == p->boundary_length) {
				if (c != CR) {
					return i;
				}
				p->index++;
				break;
			} else if (p->index == (p->boundary_length + 1)) {
				if (c != LF) {
					return i;
				}
				p->index = 0;
				NOTIFY_CB(part_data_begin);
				p->state = s_header_field_start;
				break;
			}
			if (c != p->multipart_boundary[p->index]) {
				return i;
			}
			p->index++;
			break;

		case s_header_field_start:
			multipart_log("s_header_field_start");
			mark = i;
			p->state = s_header_field;

			/* fallthrough */
		case s_header_field:
			multipart_log("s_header_field");
			if (c == CR) {
				p->state = s_headers_almost_done;
				break;
			}

			if (c == ':') {
				EMIT_DATA_CB(header_field, buf + mark,
					     i - mark);
				p->state = s_header_value_start;
				break;
			}

			cl = tolower(c);
			if ((c != '-') && (cl < 'a' || cl > 'z')) {
				multipart_log
				    ("invalid character in header name");
				return i;
			}
			if (is_last)
				EMIT_DATA_CB(header_field, buf + mark,
					     (i - mark) + 1);
			break;

		case s_headers_almost_done:
			multipart_log("s_headers_almost_done");
			if (c != LF) {
				return i;
			}

			p->state = s_part_data_start;
			break;

		case s_header_value_start:
			multipart_log("s_header_value_start");
			if (c == ' ') {
				break;
			}

			mark = i;
			p->state = s_header_value;

			/* fallthrough */
		case s_header_value:
			multipart_log("s_header_value");
			if (c == CR) {
				EMIT_DATA_CB(header_value, buf + mark,
					     i - mark);
				p->state = s_header_value_almost_done;
				break;
			}
			if (is_last)
				EMIT_DATA_CB(header_value, buf + mark,
					     (i - mark) + 1);
			break;

		case s_header_value_almost_done:
			multipart_log("s_header_value_almost_done");
			if (c != LF) {
				return i;
			}
			p->state = s_header_field_start;
			break;

		case s_part_data_start:
			multipart_log("s_part_data_start");
			NOTIFY_CB(headers_complete);
			mark = i;
			p->state = s_part_data;

			/* fallthrough */
		case s_part_data:
			multipart_log("s_part_data");
			if (c == CR) {
				EMIT_DATA_CB(part_data, buf + mark, i - mark);
				mark = i;
				p->state = s_part_data_almost_boundary;
				p->lookbehind[0] = CR;
				break;
			}
			if (is_last)
				EMIT_DATA_CB(part_data, buf + mark,
					     (i - mark) + 1);
			break;

		case s_part_data_almost_boundary:
			multipart_log("s_part_data_almost_boundary");
			if (c == LF) {
				p->state = s_part_data_boundary;
				p->lookbehind[1] = LF;
				p->index = 0;
				break;
			}
			EMIT_DATA_CB(part_data, p->lookbehind, 1);
			p->state = s_part_data;
			mark = i--;
			break;

		case s_part_data_boundary:
			multipart_log("s_part_data_boundary");
			if (p->multipart_boundary[p->index] != c) {
				EMIT_DATA_CB(part_data, p->lookbehind,
					     2 + p->index);
				p->state = s_part_data;
				mark = i--;
				break;
			}
			p->lookbehind[2 + p->index] = c;
			if ((++p->index) == p->boundary_length) {
				NOTIFY_CB(part_data_end);
				p->state = s_part_data_almost_end;
			}
			break;

		case s_part_data_almost_end:
			multipart_log("s_part_data_almost_end");
			if (c == '-') {
				p->state = s_part_data_final_hyphen;
				break;
			}
			if (c == CR) {
				p->state = s_part_data_end;
				break;
			}
			return i;

		case s_part_data_final_hyphen:
			multipart_log("s_part_data_final_hyphen");
			if (c == '-') {
				NOTIFY_CB(body_end);
				p->state = s_end;
				break;
			}
			return i;

		case s_part_data_end:
			multipart_log("s_part_data_end");
			if (c == LF) {
				p->state = s_header_field_start;
				NOTIFY_CB(part_data_begin);
				break;
			}
			return i;

		case s_end:
			multipart_log("s_end: %02X", (int)c);
			break;

		default:
			multipart_log("Multipart parser unrecoverable error");
			return 0;
		}
		++i;
	}

	return len;
}
