[PECL-CVS] [pecl-mail-mailparse] master: Decode base64/quoted-printable a block at a time

[email protected] (Ilia Alshanetsky via Remi Collet) Fri, 19 Jun 2026 06:45:48 +0000
Newsgroups php.pecl.cvs
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Committer: Remi Collet (remicollet)
Date: 2026-06-19T08:45:28+02:00

Commit: https://github.com/php/pecl-mail-mailparse/commit/934d9bdca424a8e69b9205771f02d659afcab2b3
Raw diff: https://github.com/php/pecl-mail-mailparse/commit/934d9bdca424a8e69b9205771f02d659afcab2b3.diff

Decode base64/quoted-printable a block at a time

Transfer-decoding during extraction ran one byte through two function
pointers (the per-char filter function and its output callback) and a
smart_string append for every output byte. For a multi-megabyte base64
or quoted-printable body that is millions of indirect calls.

Add buffer-at-a-time decoders that consume a whole input block in one
loop and append decoded bytes straight to the work buffer, using the
filter's status/cache as the carry state between blocks. The output is
byte-for-byte identical to the per-char path. php_mimepart_decoder_feed
and _finish now use these; the now-unused filter_into_work_buffer
output callback is removed. The per-char encode path used by
mailparse_stream_encode is unchanged.

Changed paths:
  M  mailparse_encoding.c
  M  mailparse_encoding.h
  M  php_mailparse_mime.c


Diff:

diff --git a/mailparse_encoding.c b/mailparse_encoding.c
index 76d597d..e359458 100644
--- a/mailparse_encoding.c
+++ b/mailparse_encoding.c
@@ -490,6 +490,180 @@ int mb_convert_filter_flush(mb_convert_filter *filter)
 	return 0;
 }
 
+/* =============================================================================
+ * Bulk (buffer-at-a-time) decoders
+ *
+ * These decode a whole input block in one tight loop, appending decoded bytes
+ * straight to the output smart_string. They reuse the filter's status/cache as
+ * the carry state between blocks, so the result is byte-for-byte identical to
+ * feeding the same bytes through mb_convert_filter_feed() one at a time -- but
+ * without a function-pointer call per input and per output byte, which is the
+ * dominant cost when decoding large base64/quoted-printable bodies. Only the
+ * decode directions used during MIME extraction are handled.
+ * ============================================================================= */
+
+static void mb_base64_decode_block(mb_convert_filter *filter, const char *in, size_t len, smart_string *out)
+{
+	int status = filter->status;
+	int cache = filter->cache;
+	size_t i;
+
+	for (i = 0; i < len; i++) {
+		int c = (unsigned char) in[i];
+		int n;
+
+		if (c == 0x0d || c == 0x0a || c == 0x20 || c == 0x09 || c == 0x3d) {
+			continue;	/* CR, LF, SPACE, HTAB or '=' */
+		}
+		if (c >= 0x41 && c <= 0x5a) {		/* A - Z */
+			n = c - 65;
+		} else if (c >= 0x61 && c <= 0x7a) {	/* a - z */
+			n = c - 71;
+		} else if (c >= 0x30 && c <= 0x39) {	/* 0 - 9 */
+			n = c + 4;
+		} else if (c == 0x2b) {			/* '+' */
+			n = 62;
+		} else if (c == 0x2f) {			/* '/' */
+			n = 63;
+		} else {
+			continue;			/* invalid character, ignored */
+		}
+		n &= 0x3f;
+
+		switch (status) {
+			case 0:
+				status = 1;
+				cache = n << 18;
+				break;
+			case 1:
+				status = 2;
+				cache |= n << 12;
+				break;
+			case 2:
+				status = 3;
+				cache |= n << 6;
+				break;
+			default:
+				status = 0;
+				n |= cache;
+				smart_string_appendc(out, (n >> 16) & 0xff);
+				smart_string_appendc(out, (n >> 8) & 0xff);
+				smart_string_appendc(out, n & 0xff);
+				break;
+		}
+	}
+
+	filter->status = status;
+	filter->cache = cache;
+}
+
+static void mb_base64_flush_block(mb_convert_filter *filter, smart_string *out)
+{
+	int status = filter->status;
+	int cache = filter->cache;
+
+	filter->status = 0;
+	filter->cache = 0;
+
+	if (status >= 2) {
+		smart_string_appendc(out, (cache >> 16) & 0xff);
+		if (status >= 3) {
+			smart_string_appendc(out, (cache >> 8) & 0xff);
+		}
+	}
+}
+
+static void mb_qprint_decode_block(mb_convert_filter *filter, const char *in, size_t len, smart_string *out)
+{
+	int status = filter->status;
+	int cache = filter->cache;
+	size_t i;
+
+	for (i = 0; i < len; i++) {
+		int c = (unsigned char) in[i];
+		int n, m;
+
+		switch (status) {
+			case 1:
+				if (hex2code_map[c] >= 0) {
+					cache = c;
+					status = 2;
+				} else if (c == 0x0d) {		/* soft line feed */
+					status = 3;
+				} else if (c == 0x0a) {		/* soft line feed */
+					status = 0;
+				} else {
+					smart_string_appendc(out, 0x3d);	/* '=' */
+					smart_string_appendc(out, c);
+					status = 0;
+				}
+				break;
+			case 2:
+				m = hex2code_map[c];
+				if (m < 0) {
+					smart_string_appendc(out, 0x3d);	/* '=' */
+					smart_string_appendc(out, cache);
+					n = c;
+				} else {
+					n = hex2code_map[cache] << 4 | m;
+				}
+				smart_string_appendc(out, n);
+				status = 0;
+				break;
+			case 3:
+				if (c != 0x0a) {		/* LF */
+					smart_string_appendc(out, c);
+				}
+				status = 0;
+				break;
+			default:
+				if (c == 0x3d) {		/* '=' */
+					status = 1;
+				} else {
+					smart_string_appendc(out, c);
+				}
+				break;
+		}
+	}
+
+	filter->status = status;
+	filter->cache = cache;
+}
+
+static void mb_qprint_flush_block(mb_convert_filter *filter, smart_string *out)
+{
+	int status = filter->status;
+	int cache = filter->cache;
+
+	filter->status = 0;
+	filter->cache = 0;
+
+	if (status == 1) {
+		smart_string_appendc(out, 0x3d);	/* '=' */
+	} else if (status == 2) {
+		smart_string_appendc(out, 0x3d);	/* '=' */
+		smart_string_appendc(out, cache);
+	}
+}
+
+void mb_convert_filter_feed_block(mb_convert_filter *filter, const char *in, size_t len, smart_string *out)
+{
+	if (filter->from->no_encoding == mb_no_encoding_base64) {
+		mb_base64_decode_block(filter, in, len, out);
+	} else if (filter->from->no_encoding == mb_no_encoding_qprint) {
+		mb_qprint_decode_block(filter, in, len, out);
+	}
+}
+
+void mb_convert_filter_flush_block(mb_convert_filter *filter, smart_string *out)
+{
+	if (filter->from->no_encoding == mb_no_encoding_base64) {
+		mb_base64_flush_block(filter, out);
+	} else if (filter->from->no_encoding == mb_no_encoding_qprint) {
+		mb_qprint_flush_block(filter, out);
+	}
+}
+
 /* =============================================================================
  * Encoding lookup functions
  * ============================================================================= */
diff --git a/mailparse_encoding.h b/mailparse_encoding.h
index 0ae4b77..21044f5 100644
--- a/mailparse_encoding.h
+++ b/mailparse_encoding.h
@@ -26,6 +26,7 @@
 #define MAILPARSE_ENCODING_H
 
 #include "php.h"
+#include "Zend/zend_smart_string.h"
 
 /* Encoding identifiers */
 enum mb_no_encoding {
@@ -92,6 +93,13 @@ void mb_convert_filter_delete(mb_convert_filter *filter);
 int mb_convert_filter_feed(int c, mb_convert_filter *filter);
 int mb_convert_filter_flush(mb_convert_filter *filter);
 
+/* Buffer-at-a-time decoders (BASE64 / Quoted-Printable -> 8bit). They use the
+ * filter's status/cache as carry state between blocks, so the output is
+ * identical to feeding the bytes through mb_convert_filter_feed() one at a
+ * time, without a per-byte function-pointer dispatch. */
+void mb_convert_filter_feed_block(mb_convert_filter *filter, const char *in, size_t len, smart_string *out);
+void mb_convert_filter_flush_block(mb_convert_filter *filter, smart_string *out);
+
 const mb_encoding* mb_name2encoding(const char *name);
 const mb_encoding* mb_no2encoding(enum mb_no_encoding no_encoding);
 
diff --git a/php_mailparse_mime.c b/php_mailparse_mime.c
index 6fde240..13caa99 100644
--- a/php_mailparse_mime.c
+++ b/php_mailparse_mime.c
@@ -926,21 +926,6 @@ PHP_MAILPARSE_API php_mimepart *php_mimepart_find_child_by_position(php_mimepart
 	return NULL;
 }
 
-static int filter_into_work_buffer(int c, void *dat)
-{
-	php_mimepart *part = dat;
-
-	smart_string_appendc(&part->parsedata.workbuf, c);
-
-	if (part->parsedata.workbuf.len >= 4096) {
-
-		part->extract_func(part, part->extract_context, part->parsedata.workbuf.c, part->parsedata.workbuf.len);
-		part->parsedata.workbuf.len = 0;
-	}
-
-	return c;
-}
-
 PHP_MAILPARSE_API void php_mimepart_decoder_prepare(php_mimepart *part, int do_decode, php_mimepart_extract_func_t decoder, void *ptr)
 {
 	const mb_encoding *encoding;
@@ -970,7 +955,7 @@ PHP_MAILPARSE_API void php_mimepart_decoder_prepare(php_mimepart *part, int do_d
 		} else {
 			part->extract_filter = mb_convert_filter_new(
 					mb_no2encoding(from), mb_no2encoding(mb_no_encoding_8bit),
-					filter_into_work_buffer,
+					NULL,
 					NULL,
 					part
 					);
@@ -982,8 +967,9 @@ PHP_MAILPARSE_API void php_mimepart_decoder_prepare(php_mimepart *part, int do_d
 PHP_MAILPARSE_API void php_mimepart_decoder_finish(php_mimepart *part)
 {
 	if (part->extract_filter) {
-		mb_convert_filter_flush(part->extract_filter);
+		mb_convert_filter_flush_block(part->extract_filter, &part->parsedata.workbuf);
 		mb_convert_filter_delete(part->extract_filter);
+		part->extract_filter = NULL;
 	}
 	if (part->extract_func && part->parsedata.workbuf.len > 0) {
 		part->extract_func(part, part->extract_context, part->parsedata.workbuf.c, part->parsedata.workbuf.len);
@@ -994,15 +980,11 @@ PHP_MAILPARSE_API void php_mimepart_decoder_finish(php_mimepart *part)
 PHP_MAILPARSE_API int php_mimepart_decoder_feed(php_mimepart *part, const char *buf, size_t bufsize)
 {
 	if (buf && bufsize) {
-		size_t i;
-
 		if (part->extract_filter) {
-			for (i = 0; i < bufsize; i++) {
-				if (mb_convert_filter_feed(buf[i], part->extract_filter) < 0) {
-					zend_error(E_WARNING, "%s() - filter conversion failed. Input message is probably incorrectly encoded\n",
-							get_active_function_name());
-					return -1;
-				}
+			mb_convert_filter_feed_block(part->extract_filter, buf, bufsize, &part->parsedata.workbuf);
+			if (part->parsedata.workbuf.len >= MAILPARSE_BUFSIZ) {
+				part->extract_func(part, part->extract_context, part->parsedata.workbuf.c, part->parsedata.workbuf.len);
+				part->parsedata.workbuf.len = 0;
 			}
 		} else {
 			return part->extract_func(part, part->extract_context, buf, bufsize);