/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ #include "lib.h" #include "buffer.h" #include "hex-binary.h" #include "qp-decoder.h" /* quoted-printable lines can be max 76 characters. if we've seen more than that much whitespace, it means there really shouldn't be anything else left in the line except trailing whitespace. */ #define QP_MAX_WHITESPACE_LEN 76 #define QP_IS_TRAILING_WHITESPACE(c) \ ((c) == ' ' || (c) == '\t') enum qp_state { STATE_TEXT = 0, STATE_WHITESPACE, STATE_EQUALS, STATE_EQUALS_WHITESPACE, STATE_HEX2, STATE_CR, STATE_SOFTCR }; struct qp_decoder { buffer_t *dest; buffer_t *whitespace; enum qp_state state; char hexchar; }; struct qp_decoder *qp_decoder_init(buffer_t *dest) { struct qp_decoder *qp; qp = i_new(struct qp_decoder, 1); qp->dest = dest; qp->whitespace = buffer_create_dynamic(default_pool, 80); return qp; } void qp_decoder_deinit(struct qp_decoder **_qp) { struct qp_decoder *qp = *_qp; buffer_free(&qp->whitespace); i_free(qp); } static size_t qp_decoder_more_text(struct qp_decoder *qp, const unsigned char *src, size_t src_size) { size_t i, start = 0, ret = src_size; for (i = 0; i < src_size; i++) { if (src[i] > '=') { /* fast path */ continue; } switch (src[i]) { case '=': qp->state = STATE_EQUALS; break; case '\r': qp->state = STATE_CR; break; case '\n': /* LF without preceding CR */ buffer_append(qp->dest, src+start, i-start); buffer_append(qp->dest, "\r\n", 2); start = i+1; continue; case ' ': case '\t': i_assert(qp->whitespace->used == 0); qp->state = STATE_WHITESPACE; buffer_append_c(qp->whitespace, src[i]); break; default: continue; } ret = i+1; break; } buffer_append(qp->dest, src+start, i-start); return ret; } static void qp_decoder_invalid(struct qp_decoder *qp, const char **error_r) { switch (qp->state) { case STATE_EQUALS: buffer_append_c(qp->dest, '='); *error_r = "'=' not followed by two hex digits"; break; case STATE_HEX2: buffer_append_c(qp->dest, '='); buffer_append_c(qp->dest, qp->hexchar); *error_r = "'=' not followed by a hex digit"; break; case STATE_EQUALS_WHITESPACE: buffer_append_c(qp->dest, '='); buffer_append_buf(qp->dest, qp->whitespace, 0, SIZE_MAX); buffer_set_used_size(qp->whitespace, 0); *error_r = "'=' not followed by newline"; break; case STATE_CR: buffer_append_buf(qp->dest, qp->whitespace, 0, SIZE_MAX); buffer_set_used_size(qp->whitespace, 0); buffer_append_c(qp->dest, '\r'); *error_r = "CR not followed by LF"; break; case STATE_SOFTCR: buffer_append_c(qp->dest, '='); buffer_append_buf(qp->dest, qp->whitespace, 0, SIZE_MAX); buffer_set_used_size(qp->whitespace, 0); buffer_append_c(qp->dest, '\r'); *error_r = "CR not followed by LF"; break; case STATE_TEXT: case STATE_WHITESPACE: i_unreached(); } qp->state = STATE_TEXT; i_assert(*error_r != NULL); } int qp_decoder_more(struct qp_decoder *qp, const unsigned char *src, size_t src_size, size_t *invalid_src_pos_r, const char **error_r) { const char *error; size_t i; *invalid_src_pos_r = SIZE_MAX; *error_r = NULL; for (i = 0; i < src_size; ) { switch (qp->state) { case STATE_TEXT: i += qp_decoder_more_text(qp, src+i, src_size-i); /* don't increment i any more than we already did, so continue instead of break */ continue; case STATE_WHITESPACE: if (QP_IS_TRAILING_WHITESPACE(src[i])) { /* more whitespace */ if (qp->whitespace->used <= QP_MAX_WHITESPACE_LEN) buffer_append_c(qp->whitespace, src[i]); } else if (src[i] == '\r') { qp->state = STATE_CR; } else if (src[i] == '\n') { /* drop the trailing whitespace */ buffer_append(qp->dest, "\r\n", 2); buffer_set_used_size(qp->whitespace, 0); } else { /* this wasn't trailing whitespace. put it back. */ buffer_append_buf(qp->dest, qp->whitespace, 0, SIZE_MAX); if (qp->whitespace->used > QP_MAX_WHITESPACE_LEN) { /* we already truncated some of the whitespace away, because the line is too long */ if (*invalid_src_pos_r == SIZE_MAX) { *invalid_src_pos_r = i; *error_r = "Too much whitespace"; } } buffer_set_used_size(qp->whitespace, 0); qp->state = STATE_TEXT; continue; /* don't increment i */ } break; case STATE_EQUALS: if ((src[i] >= '0' && src[i] <= '9') || (src[i] >= 'A' && src[i] <= 'F') || /* lowercase hex isn't strictly valid, but allow */ (src[i] >= 'a' && src[i] <= 'f')) { qp->hexchar = src[i]; qp->state = STATE_HEX2; } else if (QP_IS_TRAILING_WHITESPACE(src[i])) { i_assert(qp->whitespace->used == 0); buffer_append_c(qp->whitespace, src[i]); qp->state = STATE_EQUALS_WHITESPACE; } else if (src[i] == '\r') qp->state = STATE_SOFTCR; else if (src[i] == '\n') { qp->state = STATE_TEXT; } else { /* invalid input */ qp_decoder_invalid(qp, &error); if (*invalid_src_pos_r == SIZE_MAX) { *invalid_src_pos_r = i; *error_r = error; } continue; /* don't increment i */ } break; case STATE_HEX2: if ((src[i] >= '0' && src[i] <= '9') || (src[i] >= 'A' && src[i] <= 'F') || (src[i] >= 'a' && src[i] <= 'f')) { char data[3]; data[0] = qp->hexchar; data[1] = src[i]; data[2] = '\0'; if (hex_to_binary(data, qp->dest) < 0) i_unreached(); qp->state = STATE_TEXT; } else { /* invalid input */ qp_decoder_invalid(qp, &error); if (*invalid_src_pos_r == SIZE_MAX) { *invalid_src_pos_r = i; *error_r = error; } continue; /* don't increment i */ } break; case STATE_EQUALS_WHITESPACE: if (QP_IS_TRAILING_WHITESPACE(src[i])) { if (qp->whitespace->used <= QP_MAX_WHITESPACE_LEN) buffer_append_c(qp->whitespace, src[i]); else { /* if this isn't going to get truncated anyway, it's going to be an error */ } } else if (src[i] == '\r') qp->state = STATE_SOFTCR; else if (src[i] == '\n') { buffer_set_used_size(qp->whitespace, 0); qp->state = STATE_TEXT; } else { /* = not followed by [CR]LF is invalid. */ qp_decoder_invalid(qp, &error); if (*invalid_src_pos_r == SIZE_MAX) { *invalid_src_pos_r = i; *error_r = error; } continue; /* don't increment i */ } break; case STATE_CR: case STATE_SOFTCR: if (src[i] == '\n') { buffer_set_used_size(qp->whitespace, 0); if (qp->state != STATE_SOFTCR) buffer_append(qp->dest, "\r\n", 2); qp->state = STATE_TEXT; } else { qp_decoder_invalid(qp, &error); if (*invalid_src_pos_r == SIZE_MAX) { *invalid_src_pos_r = i; *error_r = error; } continue; /* don't increment i */ } break; } i++; } i_assert((*invalid_src_pos_r == SIZE_MAX) == (*error_r == NULL)); return *invalid_src_pos_r == SIZE_MAX ? 0 : -1; } int qp_decoder_finish(struct qp_decoder *qp, const char **error_r) { int ret; if (qp->state == STATE_TEXT || qp->state == STATE_WHITESPACE) { ret = 0; *error_r = NULL; } else { qp_decoder_invalid(qp, error_r); ret = -1; } qp->state = STATE_TEXT; buffer_set_used_size(qp->whitespace, 0); return ret; }