diff --git a/ext/encoding/config.m4 b/ext/encoding/config.m4
new file mode 100644
index 000000000000..05136e3f307c
--- /dev/null
+++ b/ext/encoding/config.m4
@@ -0,0 +1,6 @@
+PHP_NEW_EXTENSION([encoding], m4_normalize([
+ encoding.c
+ ]),
+ [no])
+
+PHP_INSTALL_HEADERS([ext/encoding], [encoding_decl.h])
diff --git a/ext/encoding/encoding.c b/ext/encoding/encoding.c
new file mode 100644
index 000000000000..6728a4b79ba9
--- /dev/null
+++ b/ext/encoding/encoding.c
@@ -0,0 +1,1556 @@
+/*
+ +----------------------------------------------------------------------+
+ | Copyright © The PHP Group and Contributors. |
+ +----------------------------------------------------------------------+
+ | This source file is subject to the Modified BSD License that is |
+ | bundled with this package in the file LICENSE, and is available |
+ | through the World Wide Web at . |
+ | |
+ | SPDX-License-Identifier: BSD-3-Clause |
+ +----------------------------------------------------------------------+
+ | Author: Ignace Nyamagana Butera |
+ +----------------------------------------------------------------------+
+ */
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include "php.h"
+#include "Zend/zend_enum.h"
+#include "Zend/zend_exceptions.h"
+#include "encoding_arginfo.h"
+#include "encoding_decl.h"
+
+#if defined(__GNUC__) || defined(__clang__)
+#pragma GCC diagnostic ignored "-Woverride-init"
+#endif
+
+/* Class entries */
+static zend_class_entry *encoding_ce_EncodingError;
+static zend_class_entry *encoding_ce_EncodingException;
+static zend_class_entry *encoding_ce_UnableToDecodeException;
+static zend_class_entry *encoding_ce_UnableToEncodeException;
+
+static zend_class_entry *encoding_ce_Base16;
+static zend_class_entry *encoding_ce_Base32;
+static zend_class_entry *encoding_ce_Base58;
+static zend_class_entry *encoding_ce_Base64;
+static zend_class_entry *encoding_ce_Base85;
+static zend_class_entry *encoding_ce_PaddingMode;
+static zend_class_entry *encoding_ce_DecodingMode;
+static zend_class_entry *encoding_ce_TimingMode;
+
+/* Exception throwing macros */
+#define THROW_UNABLE_TO_DECODE(msg) \
+ zend_throw_exception(encoding_ce_UnableToDecodeException, msg, 0)
+
+#define THROW_UNABLE_TO_ENCODE(msg) \
+ zend_throw_exception(encoding_ce_UnableToEncodeException, msg, 0)
+
+/* ===================== Base16 ===================== */
+
+/* Lookup tables for Base16 encoding */
+static const uint8_t base16_encode_upper[] = {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
+};
+
+static const uint8_t base16_encode_lower[] = {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
+};
+
+/* Reverse lookup for decoding: maps ASCII char to 0-15 or 0xFF if invalid */
+static const uint8_t base16_reverse_upper[256] = {
+ [0 ... 255] = 0xFF,
+ ['0'] = 0, ['1'] = 1, ['2'] = 2, ['3'] = 3, ['4'] = 4, ['5'] = 5,
+ ['6'] = 6, ['7'] = 7, ['8'] = 8, ['9'] = 9,
+ ['A'] = 10, ['B'] = 11, ['C'] = 12, ['D'] = 13, ['E'] = 14, ['F'] = 15
+};
+
+static const uint8_t base16_reverse_lower[256] = {
+ [0 ... 255] = 0xFF,
+ ['0'] = 0, ['1'] = 1, ['2'] = 2, ['3'] = 3, ['4'] = 4, ['5'] = 5,
+ ['6'] = 6, ['7'] = 7, ['8'] = 8, ['9'] = 9,
+ ['a'] = 10, ['b'] = 11, ['c'] = 12, ['d'] = 13, ['e'] = 14, ['f'] = 15
+};
+
+static const uint8_t base16_reverse_forgiving[256] = {
+ [0 ... 255] = 0xFF,
+ ['0'] = 0, ['1'] = 1, ['2'] = 2, ['3'] = 3, ['4'] = 4, ['5'] = 5,
+ ['6'] = 6, ['7'] = 7, ['8'] = 8, ['9'] = 9,
+ ['A'] = 10, ['B'] = 11, ['C'] = 12, ['D'] = 13, ['E'] = 14, ['F'] = 15,
+ ['a'] = 10, ['b'] = 11, ['c'] = 12, ['d'] = 13, ['e'] = 14, ['f'] = 15
+};
+
+static inline bool is_base16_whitespace(uint8_t c) {
+ return c == ' ' || c == '\t' || c == '\n' || c == '\r';
+}
+
+static zend_string *base16_encode_impl(const char *data, size_t len, zend_enum_Encoding_Base16 variant, bool constant_time) {
+ const uint8_t *table = (variant == ZEND_ENUM_Encoding_Base16_Upper)
+ ? base16_encode_upper : base16_encode_lower;
+
+ zend_string *result = zend_string_alloc(len * 2, 0);
+ char *dst = ZSTR_VAL(result);
+
+ for (size_t i = 0; i < len; i++) {
+ uint8_t byte = (uint8_t)data[i];
+ *dst++ = table[byte >> 4];
+ *dst++ = table[byte & 0x0F];
+ }
+
+ ZSTR_LEN(result) = len * 2;
+ ZSTR_VAL(result)[len * 2] = '\0';
+ return result;
+}
+
+static zend_string *base16_decode_impl(const char *data, size_t len, zend_enum_Encoding_Base16 variant, zend_enum_Encoding_DecodingMode mode, bool constant_time) {
+ const uint8_t *reverse_table;
+
+ if (mode == ZEND_ENUM_Encoding_DecodingMode_Forgiving) {
+ reverse_table = base16_reverse_forgiving;
+ } else if (variant == ZEND_ENUM_Encoding_Base16_Upper) {
+ reverse_table = base16_reverse_upper;
+ } else {
+ reverse_table = base16_reverse_lower;
+ }
+
+ /* Count non-whitespace characters to determine output length */
+ size_t content_len = 0;
+ for (size_t i = 0; i < len; i++) {
+ if (!is_base16_whitespace((uint8_t)data[i])) {
+ content_len++;
+ }
+ }
+
+ /* Must have even number of hex digits */
+ if (content_len % 2 != 0) {
+ THROW_UNABLE_TO_DECODE("Hex data must have an even length");
+ return NULL;
+ }
+
+ zend_string *result = zend_string_alloc(content_len / 2, 0);
+ char *dst = ZSTR_VAL(result);
+ size_t dst_idx = 0;
+ uint8_t high = 0;
+ bool have_high = false;
+
+ for (size_t i = 0; i < len; i++) {
+ uint8_t c = (uint8_t)data[i];
+
+ if (is_base16_whitespace(c)) {
+ continue;
+ }
+
+ uint8_t val = reverse_table[c];
+
+ if (val == 0xFF) {
+ zend_string_efree(result);
+ THROW_UNABLE_TO_DECODE("Invalid hex character");
+ return NULL;
+ }
+
+ if (!have_high) {
+ high = val;
+ have_high = true;
+ } else {
+ dst[dst_idx++] = (char)(high << 4 | val);
+ have_high = false;
+ }
+ }
+
+ ZSTR_LEN(result) = dst_idx;
+ ZSTR_VAL(result)[dst_idx] = '\0';
+ return result;
+}
+
+ PHP_FUNCTION(Encoding_base16_encode) {
+ char *data;
+ size_t data_len;
+ zval *variant_obj = NULL;
+ zval *timing_obj = NULL;
+
+ ZEND_PARSE_PARAMETERS_START(1, 3)
+ Z_PARAM_STRING(data, data_len)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_OBJECT_OF_CLASS(variant_obj, encoding_ce_Base16)
+ Z_PARAM_OBJECT_OF_CLASS(timing_obj, encoding_ce_TimingMode)
+ ZEND_PARSE_PARAMETERS_END();
+
+ zend_enum_Encoding_Base16 variant = ZEND_ENUM_Encoding_Base16_Upper;
+ if (variant_obj) {
+ variant = (zend_enum_Encoding_Base16)zend_enum_fetch_case_id(Z_OBJ_P(variant_obj));
+ }
+
+ bool constant_time = false;
+ if (timing_obj) {
+ constant_time = (zend_enum_fetch_case_id(Z_OBJ_P(timing_obj)) == ZEND_ENUM_Encoding_TimingMode_Constant);
+ }
+
+ RETURN_STR(base16_encode_impl(data, data_len, variant, constant_time));
+}
+
+ PHP_FUNCTION(Encoding_base16_decode) {
+ char *data;
+ size_t data_len;
+ zval *variant_obj = NULL;
+ zval *decoding_obj = NULL;
+ zval *timing_obj = NULL;
+
+ ZEND_PARSE_PARAMETERS_START(1, 4)
+ Z_PARAM_STRING(data, data_len)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_OBJECT_OF_CLASS(variant_obj, encoding_ce_Base16)
+ Z_PARAM_OBJECT_OF_CLASS(decoding_obj, encoding_ce_DecodingMode)
+ Z_PARAM_OBJECT_OF_CLASS(timing_obj, encoding_ce_TimingMode)
+ ZEND_PARSE_PARAMETERS_END();
+
+ zend_enum_Encoding_Base16 variant = ZEND_ENUM_Encoding_Base16_Upper;
+ if (variant_obj) {
+ variant = (zend_enum_Encoding_Base16)zend_enum_fetch_case_id(Z_OBJ_P(variant_obj));
+ }
+
+ zend_enum_Encoding_DecodingMode mode = ZEND_ENUM_Encoding_DecodingMode_Strict;
+ if (decoding_obj) {
+ mode = (zend_enum_Encoding_DecodingMode)zend_enum_fetch_case_id(Z_OBJ_P(decoding_obj));
+ }
+
+ bool constant_time = false;
+ if (timing_obj) {
+ constant_time = (zend_enum_fetch_case_id(Z_OBJ_P(timing_obj)) == ZEND_ENUM_Encoding_TimingMode_Constant);
+ }
+
+ zend_string *result = base16_decode_impl(data, data_len, variant, mode, constant_time);
+ if (!result) {
+ return;
+ }
+ RETURN_STR(result);
+}
+
+/* ===================== Base32 ===================== */
+
+static const char base32_ascii_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
+static const char base32_hex_table[] = "0123456789ABCDEFGHIJKLMNOPQRSTUV";
+static const char base32_crockford_table[] = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
+static const char base32_z_table[] = "pebrv9tkngxm4zywsiu1oca6fjh5q23780ld";
+
+static const uint8_t base32_ascii_reverse[256] = {
+ [0 ... 255] = 0xFF,
+ ['A'] = 0, ['B'] = 1, ['C'] = 2, ['D'] = 3, ['E'] = 4,
+ ['F'] = 5, ['G'] = 6, ['H'] = 7, ['I'] = 8, ['J'] = 9,
+ ['K'] = 10, ['L'] = 11, ['M'] = 12, ['N'] = 13, ['O'] = 14,
+ ['P'] = 15, ['Q'] = 16, ['R'] = 17, ['S'] = 18, ['T'] = 19,
+ ['U'] = 20, ['V'] = 21, ['W'] = 22, ['X'] = 23, ['Y'] = 24,
+ ['Z'] = 25, ['2'] = 26, ['3'] = 27, ['4'] = 28, ['5'] = 29,
+ ['6'] = 30, ['7'] = 31
+};
+
+static const uint8_t base32_hex_reverse[256] = {
+ [0 ... 255] = 0xFF,
+ ['0'] = 0, ['1'] = 1, ['2'] = 2, ['3'] = 3, ['4'] = 4,
+ ['5'] = 5, ['6'] = 6, ['7'] = 7, ['8'] = 8, ['9'] = 9,
+ ['A'] = 10, ['B'] = 11, ['C'] = 12, ['D'] = 13, ['E'] = 14,
+ ['F'] = 15, ['G'] = 16, ['H'] = 17, ['I'] = 18, ['J'] = 19,
+ ['K'] = 20, ['L'] = 21, ['M'] = 22, ['N'] = 23, ['O'] = 24,
+ ['P'] = 25, ['Q'] = 26, ['R'] = 27, ['S'] = 28, ['T'] = 29,
+ ['U'] = 30, ['V'] = 31
+};
+
+static const uint8_t base32_crockford_reverse[256] = {
+ [0 ... 255] = 0xFF,
+ ['0'] = 0, ['1'] = 1, ['2'] = 2, ['3'] = 3, ['4'] = 4,
+ ['5'] = 5, ['6'] = 6, ['7'] = 7, ['8'] = 8, ['9'] = 9,
+ ['A'] = 10, ['B'] = 11, ['C'] = 12, ['D'] = 13, ['E'] = 14,
+ ['F'] = 15, ['G'] = 16, ['H'] = 17,
+ /* Crockford: I and L decode as 1, O and o decode as 0 */
+ ['I'] = 1, ['i'] = 1, ['L'] = 1, ['l'] = 1,
+ ['O'] = 0, ['o'] = 0,
+ ['J'] = 18, ['K'] = 19,
+ ['M'] = 20, ['N'] = 21, ['P'] = 22,
+ ['Q'] = 23, ['R'] = 24, ['S'] = 25, ['T'] = 26,
+ ['V'] = 27, ['W'] = 28, ['X'] = 29, ['Y'] = 30, ['Z'] = 31,
+ /* Lowercase for case-insensitive Crockford */
+ ['a'] = 10, ['b'] = 11, ['c'] = 12, ['d'] = 13, ['e'] = 14,
+ ['f'] = 15, ['g'] = 16, ['h'] = 17,
+ ['j'] = 18, ['k'] = 19,
+ ['m'] = 20, ['n'] = 21, ['p'] = 22,
+ ['q'] = 23, ['r'] = 24, ['s'] = 25, ['t'] = 26,
+ ['v'] = 27, ['w'] = 28, ['x'] = 29, ['y'] = 30, ['z'] = 31
+};
+
+static const uint8_t base32_z_reverse[256] = {
+ [0 ... 255] = 0xFF,
+ ['p'] = 0, ['e'] = 1, ['b'] = 2, ['r'] = 3, ['v'] = 4,
+ ['9'] = 5, ['t'] = 6, ['k'] = 7, ['n'] = 8, ['g'] = 9,
+ ['x'] = 10, ['m'] = 11, ['4'] = 12, ['z'] = 13, ['y'] = 14,
+ ['w'] = 15, ['s'] = 16, ['i'] = 17, ['u'] = 18, ['1'] = 19,
+ ['o'] = 20, ['c'] = 21, ['a'] = 22, ['6'] = 23, ['f'] = 24,
+ ['j'] = 25, ['h'] = 26, ['5'] = 27, ['q'] = 28, ['2'] = 29,
+ ['3'] = 30, ['7'] = 31, ['8'] = 32, ['0'] = 33, ['l'] = 34, ['d'] = 35
+};
+
+static inline bool is_base32_whitespace(uint8_t c) {
+ return c == ' ' || c == '\t' || c == '\n' || c == '\r';
+}
+
+static zend_string *base32_encode_impl(const char *data, size_t len, zend_enum_Encoding_Base32 variant, zend_enum_Encoding_PaddingMode padding) {
+ const char *table;
+ bool uses_padding;
+ bool strip_padding;
+
+ switch (variant) {
+ case ZEND_ENUM_Encoding_Base32_Ascii:
+ table = base32_ascii_table;
+ uses_padding = true;
+ break;
+ case ZEND_ENUM_Encoding_Base32_Hex:
+ table = base32_hex_table;
+ uses_padding = true;
+ break;
+ case ZEND_ENUM_Encoding_Base32_Crockford:
+ table = base32_crockford_table;
+ uses_padding = false;
+ break;
+ case ZEND_ENUM_Encoding_Base32_Z:
+ table = base32_z_table;
+ uses_padding = false;
+ break;
+ default:
+ table = base32_ascii_table;
+ uses_padding = true;
+ break;
+ }
+
+ switch (padding) {
+ case ZEND_ENUM_Encoding_PaddingMode_VariantControlled:
+ strip_padding = false;
+ break;
+ case ZEND_ENUM_Encoding_PaddingMode_StripPadding:
+ strip_padding = true;
+ if (!uses_padding) {
+ zend_value_error("PaddingMode::StripPadding is not supported for variant %s",
+ variant == ZEND_ENUM_Encoding_Base32_Crockford ? "Base32::Crockford" :
+ variant == ZEND_ENUM_Encoding_Base32_Z ? "Base32::Z" : "Base32");
+ return NULL;
+ }
+ break;
+ case ZEND_ENUM_Encoding_PaddingMode_PreservePadding:
+ strip_padding = false;
+ if (!uses_padding) {
+ zend_value_error("PaddingMode::PreservePadding is not supported for variant %s",
+ variant == ZEND_ENUM_Encoding_Base32_Crockford ? "Base32::Crockford" :
+ variant == ZEND_ENUM_Encoding_Base32_Z ? "Base32::Z" : "Base32");
+ return NULL;
+ }
+ break;
+ default:
+ strip_padding = false;
+ break;
+ }
+
+ size_t encoded_len = (len + 4) / 5 * 8;
+ if (strip_padding) {
+ size_t remainder = len % 5;
+ if (remainder) {
+ size_t pad = 8 - ((remainder * 8 + 4) / 5);
+ encoded_len -= pad;
+ }
+ }
+
+ zend_string *result = zend_string_alloc(encoded_len, 0);
+ char *dst = ZSTR_VAL(result);
+
+ size_t i = 0;
+ while (i < len) {
+ uint64_t nbits = 0;
+ size_t bytes_left = len - i;
+ size_t chars_to_read = bytes_left < 5 ? bytes_left : 5;
+
+ for (size_t j = 0; j < chars_to_read; j++) {
+ nbits = (nbits << 8) | (uint8_t)data[i + j];
+ }
+
+ size_t total_chars = (chars_to_read * 8 + 4) / 5;
+ nbits <<= (uint64_t)(total_chars * 5 - chars_to_read * 8);
+ for (size_t c = 0; c < total_chars; c++) {
+ *dst++ = table[(nbits >> ((total_chars - 1 - c) * 5)) & 0x1F];
+ }
+
+ if (uses_padding && !strip_padding && chars_to_read < 5) {
+ size_t pad = 8 - total_chars;
+ for (size_t p = 0; p < pad; p++) {
+ *dst++ = '=';
+ }
+ }
+
+ i += chars_to_read;
+ }
+
+ size_t result_len = (size_t)(dst - ZSTR_VAL(result));
+ ZSTR_LEN(result) = result_len;
+ ZSTR_VAL(result)[result_len] = '\0';
+ return result;
+}
+
+static zend_string *base32_decode_impl(const char *data, size_t len, zend_enum_Encoding_Base32 variant, zend_enum_Encoding_DecodingMode mode, bool constant_time) {
+ const uint8_t *reverse_table;
+ bool uses_padding;
+
+ switch (variant) {
+ case ZEND_ENUM_Encoding_Base32_Ascii:
+ reverse_table = base32_ascii_reverse;
+ uses_padding = true;
+ break;
+ case ZEND_ENUM_Encoding_Base32_Hex:
+ reverse_table = base32_hex_reverse;
+ uses_padding = true;
+ break;
+ case ZEND_ENUM_Encoding_Base32_Crockford:
+ reverse_table = base32_crockford_reverse;
+ uses_padding = false;
+ break;
+ case ZEND_ENUM_Encoding_Base32_Z:
+ reverse_table = base32_z_reverse;
+ uses_padding = false;
+ break;
+ default:
+ reverse_table = base32_ascii_reverse;
+ uses_padding = true;
+ break;
+ }
+
+ /* Count non-whitespace, non-padding characters */
+ size_t content_len = 0;
+ size_t padding_count = 0;
+ for (size_t i = 0; i < len; i++) {
+ uint8_t c = (uint8_t)data[i];
+ if (is_base32_whitespace(c)) {
+ continue;
+ }
+ if (c == '=') {
+ padding_count++;
+ continue;
+ }
+ content_len++;
+ }
+
+ bool forgiving = (mode == ZEND_ENUM_Encoding_DecodingMode_Forgiving);
+
+ if (uses_padding) {
+ if (forgiving) {
+ /* In forgiving mode, we can handle missing or incorrect padding */
+ /* Calculate expected padding and fix it */
+ size_t remainder = content_len % 8;
+ if (remainder) {
+ size_t expected_pad = 8 - remainder;
+ if (padding_count != expected_pad && padding_count == 0) {
+ /* Missing padding - forgive it */
+ } else if (padding_count != expected_pad) {
+ THROW_UNABLE_TO_DECODE("Invalid padding length");
+ return NULL;
+ }
+ } else if (padding_count != 0) {
+ THROW_UNABLE_TO_DECODE("Invalid padding length");
+ return NULL;
+ }
+ } else {
+ /* Strict mode: padding is required */
+ size_t remainder = content_len % 8;
+ if (remainder) {
+ size_t expected_pad = 8 - remainder;
+ if (padding_count != expected_pad) {
+ THROW_UNABLE_TO_DECODE("Invalid padding length");
+ return NULL;
+ }
+ } else if (padding_count != 0) {
+ THROW_UNABLE_TO_DECODE("Invalid padding length");
+ return NULL;
+ }
+ }
+ }
+
+ size_t out_len = (content_len * 5) / 8;
+ zend_string *result = zend_string_alloc(out_len, 0);
+ char *dst = ZSTR_VAL(result);
+ size_t dst_idx = 0;
+
+ uint64_t nbits = 0;
+ int bits = 0;
+
+ for (size_t i = 0; i < len; i++) {
+ uint8_t c = (uint8_t)data[i];
+
+ if (is_base32_whitespace(c) || c == '=') {
+ continue;
+ }
+
+ uint8_t val = reverse_table[c];
+
+ if (val == 0xFF) {
+ zend_string_efree(result);
+ THROW_UNABLE_TO_DECODE("Invalid Base32 character");
+ return NULL;
+ }
+
+ nbits = (nbits << 5) | val;
+ bits += 5;
+
+ while (bits >= 8) {
+ bits -= 8;
+ if (dst_idx < out_len) {
+ dst[dst_idx++] = (char)((nbits >> bits) & 0xFF);
+ }
+ }
+ }
+
+ ZSTR_LEN(result) = dst_idx;
+ ZSTR_VAL(result)[dst_idx] = '\0';
+ return result;
+}
+
+PHP_FUNCTION(Encoding_base32_encode) {
+ char *data;
+ size_t data_len;
+ zval *variant_obj = NULL;
+ zval *padding_obj = NULL;
+ zval *timing_obj = NULL;
+
+ ZEND_PARSE_PARAMETERS_START(1, 4)
+ Z_PARAM_STRING(data, data_len)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_OBJECT_OF_CLASS(variant_obj, encoding_ce_Base32)
+ Z_PARAM_OBJECT_OF_CLASS(padding_obj, encoding_ce_PaddingMode)
+ Z_PARAM_OBJECT_OF_CLASS(timing_obj, encoding_ce_TimingMode)
+ ZEND_PARSE_PARAMETERS_END();
+
+ zend_enum_Encoding_Base32 variant = ZEND_ENUM_Encoding_Base32_Ascii;
+ if (variant_obj) {
+ variant = (zend_enum_Encoding_Base32)zend_enum_fetch_case_id(Z_OBJ_P(variant_obj));
+ }
+
+ zend_enum_Encoding_PaddingMode padding = ZEND_ENUM_Encoding_PaddingMode_VariantControlled;
+ if (padding_obj) {
+ padding = (zend_enum_Encoding_PaddingMode)zend_enum_fetch_case_id(Z_OBJ_P(padding_obj));
+ }
+
+ zend_string *result = base32_encode_impl(data, data_len, variant, padding);
+ if (!result) {
+ return;
+ }
+ RETURN_STR(result);
+}
+
+PHP_FUNCTION(Encoding_base32_decode) {
+ char *data;
+ size_t data_len;
+ zval *variant_obj = NULL;
+ zval *decoding_obj = NULL;
+ zval *timing_obj = NULL;
+
+ ZEND_PARSE_PARAMETERS_START(1, 4)
+ Z_PARAM_STRING(data, data_len)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_OBJECT_OF_CLASS(variant_obj, encoding_ce_Base32)
+ Z_PARAM_OBJECT_OF_CLASS(decoding_obj, encoding_ce_DecodingMode)
+ Z_PARAM_OBJECT_OF_CLASS(timing_obj, encoding_ce_TimingMode)
+ ZEND_PARSE_PARAMETERS_END();
+
+ zend_enum_Encoding_Base32 variant = ZEND_ENUM_Encoding_Base32_Ascii;
+ if (variant_obj) {
+ variant = (zend_enum_Encoding_Base32)zend_enum_fetch_case_id(Z_OBJ_P(variant_obj));
+ }
+
+ zend_enum_Encoding_DecodingMode mode = ZEND_ENUM_Encoding_DecodingMode_Strict;
+ if (decoding_obj) {
+ mode = (zend_enum_Encoding_DecodingMode)zend_enum_fetch_case_id(Z_OBJ_P(decoding_obj));
+ }
+
+ bool constant_time = false;
+ if (timing_obj) {
+ constant_time = (zend_enum_fetch_case_id(Z_OBJ_P(timing_obj)) == ZEND_ENUM_Encoding_TimingMode_Constant);
+ }
+
+ zend_string *result = base32_decode_impl(data, data_len, variant, mode, constant_time);
+ if (!result) {
+ return;
+ }
+ RETURN_STR(result);
+}
+
+/* ===================== Base58 ===================== */
+
+static const char base58_bitcoin_table[] = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
+static const char base58_flickr_table[] = "123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";
+
+static const uint8_t base58_bitcoin_reverse[256] = {
+ [0 ... 255] = 0xFF,
+ ['1'] = 0, ['2'] = 1, ['3'] = 2, ['4'] = 3, ['5'] = 4,
+ ['6'] = 5, ['7'] = 6, ['8'] = 7, ['9'] = 8,
+ ['A'] = 9, ['B'] = 10, ['C'] = 11, ['D'] = 12, ['E'] = 13, ['F'] = 14, ['G'] = 15,
+ ['H'] = 16, ['J'] = 17, ['K'] = 18, ['L'] = 19,
+ ['M'] = 20, ['N'] = 21,
+ ['P'] = 22, ['Q'] = 23, ['R'] = 24, ['S'] = 25, ['T'] = 26, ['U'] = 27,
+ ['V'] = 28, ['W'] = 29, ['X'] = 30, ['Y'] = 31, ['Z'] = 32,
+ ['a'] = 33, ['b'] = 34, ['c'] = 35, ['d'] = 36, ['e'] = 37, ['f'] = 38,
+ ['g'] = 39, ['h'] = 40, ['i'] = 41, ['j'] = 42, ['k'] = 43, ['m'] = 44,
+ ['n'] = 45, ['o'] = 46, ['p'] = 47, ['q'] = 48, ['r'] = 49, ['s'] = 50, ['t'] = 51,
+ ['u'] = 52, ['v'] = 53, ['w'] = 54, ['x'] = 55, ['y'] = 56, ['z'] = 57
+};
+
+static const uint8_t base58_flickr_reverse[256] = {
+ [0 ... 255] = 0xFF,
+ ['1'] = 0, ['2'] = 1, ['3'] = 2, ['4'] = 3, ['5'] = 4,
+ ['6'] = 5, ['7'] = 6, ['8'] = 7, ['9'] = 8,
+ ['a'] = 9, ['b'] = 10, ['c'] = 11, ['d'] = 12, ['e'] = 13, ['f'] = 14, ['g'] = 15,
+ ['h'] = 16, ['i'] = 17, ['j'] = 18, ['k'] = 19,
+ ['m'] = 20, ['n'] = 21, ['o'] = 22,
+ ['p'] = 23, ['q'] = 24, ['r'] = 25, ['s'] = 26, ['t'] = 27, ['u'] = 28,
+ ['v'] = 29, ['w'] = 30, ['x'] = 31, ['y'] = 32, ['z'] = 33,
+ ['A'] = 34, ['B'] = 35, ['C'] = 36, ['D'] = 37, ['E'] = 38, ['F'] = 39,
+ ['G'] = 40, ['H'] = 41, ['J'] = 42, ['K'] = 43, ['L'] = 44, ['M'] = 45,
+ ['N'] = 46, ['P'] = 47, ['Q'] = 48, ['R'] = 49, ['S'] = 50, ['T'] = 51,
+ ['U'] = 52, ['V'] = 53, ['W'] = 54, ['X'] = 55, ['Y'] = 56, ['Z'] = 57
+};
+
+static zend_string *base58_encode_impl(const char *data, size_t len, zend_enum_Encoding_Base58 variant) {
+ const char *table = (variant == ZEND_ENUM_Encoding_Base58_Bitcoin)
+ ? base58_bitcoin_table : base58_flickr_table;
+
+ /* Max output size: ceil(len * 8 / log2(58)) ≈ len * 1.4 + margin */
+ size_t max_out_len = (len * 138 + 100) / 100 + 16;
+
+ /* Count leading zeros */
+ size_t zeros = 0;
+ while (zeros < len && (uint8_t)data[zeros] == 0) {
+ zeros++;
+ }
+
+ /* Output buffer, zero-initialized */
+ uint8_t *output = ecalloc(max_out_len, sizeof(uint8_t));
+ size_t out_begin = max_out_len;
+ size_t out_end = max_out_len;
+
+ /* Process each input byte: multiply accumulated number by 256, add byte */
+ for (size_t i = zeros; i < len; i++) {
+ uint32_t carry = (uint8_t)data[i];
+ size_t j = out_end;
+ while (j > out_begin) {
+ j--;
+ carry += (uint32_t)output[j] * 256;
+ output[j] = (uint8_t)(carry % 58);
+ carry /= 58;
+ }
+ while (carry > 0) {
+ if (out_begin == 0) break;
+ out_begin--;
+ output[out_begin] = (uint8_t)(carry % 58);
+ carry /= 58;
+ }
+ }
+
+ /* Calculate output length */
+ size_t out_len = zeros + (out_end - out_begin);
+ zend_string *result = zend_string_alloc(out_len, 0);
+ char *dst = ZSTR_VAL(result);
+
+ /* Leading zeros become '1' characters */
+ for (size_t i = 0; i < zeros; i++) {
+ *dst++ = table[0];
+ }
+
+ /* Encode the digits */
+ for (size_t i = out_begin; i < out_end; i++) {
+ *dst++ = table[output[i]];
+ }
+
+ size_t result_len = (size_t)(dst - ZSTR_VAL(result));
+ ZSTR_LEN(result) = result_len;
+ ZSTR_VAL(result)[result_len] = '\0';
+ efree(output);
+
+ return result;
+}
+
+static zend_string *base58_decode_impl(const char *data, size_t len, zend_enum_Encoding_Base58 variant) {
+ const uint8_t *reverse_table = (variant == ZEND_ENUM_Encoding_Base58_Bitcoin)
+ ? base58_bitcoin_reverse : base58_flickr_reverse;
+
+ /* Count leading '1' characters (zeros in base58) and validate all chars */
+ size_t zeros = 0;
+ size_t content_len = 0;
+ bool past_leading = false;
+ for (size_t i = 0; i < len; i++) {
+ uint8_t c = (uint8_t)data[i];
+ if (c == ' ' || c == '\t' || c == '\n' || c == '\r') continue;
+ uint8_t val = reverse_table[c];
+ if (val == 0xFF) {
+ THROW_UNABLE_TO_DECODE("Invalid Base58 character");
+ return NULL;
+ }
+ if (!past_leading) {
+ if (val == 0) zeros++;
+ else past_leading = true;
+ }
+ content_len++;
+ }
+
+ /* Parse non-zero digits into base58 digit array (LSB first) */
+ size_t num_digits = content_len - zeros;
+ if (num_digits == 0) {
+ zend_string *result = zend_string_alloc(zeros, 0);
+ memset(ZSTR_VAL(result), 0, zeros);
+ ZSTR_VAL(result)[zeros] = '\0';
+ return result;
+ }
+
+ uint8_t *digits = emalloc(num_digits);
+ size_t di = 0;
+ past_leading = false;
+ for (size_t i = 0; i < len; i++) {
+ uint8_t c = (uint8_t)data[i];
+ if (c == ' ' || c == '\t' || c == '\n' || c == '\r') continue;
+ if (!past_leading) {
+ if (reverse_table[c] == 0) continue;
+ past_leading = true;
+ }
+ digits[di++] = reverse_table[c];
+ }
+
+ /* digits[] holds the base58 number with digits[0] = MSD, digits[num_digits-1] = LSD */
+ /* Reverse to get LSD first */
+ for (size_t i = 0; i < num_digits / 2; i++) {
+ uint8_t tmp = digits[i];
+ digits[i] = digits[num_digits - 1 - i];
+ digits[num_digits - 1 - i] = tmp;
+ }
+
+ /* Repeatedly divide by 256, collecting remainders as output bytes */
+ size_t max_out_len = (content_len * 74 + 100) / 100 + 1;
+ uint8_t *output = ecalloc(max_out_len, sizeof(uint8_t));
+ size_t out_idx = 0;
+
+ size_t d_begin = 0;
+ size_t d_end = num_digits;
+
+ while (d_begin < d_end) {
+ uint32_t carry = 0;
+ for (size_t j = d_end; j > d_begin; ) {
+ j--;
+ uint32_t temp = carry * 58 + digits[j];
+ digits[j] = (uint8_t)(temp / 256);
+ carry = temp % 256;
+ }
+ if (out_idx < max_out_len) {
+ output[out_idx++] = (uint8_t)carry;
+ }
+ /* Skip trailing zeros (MSD side) in digit array */
+ while (d_begin < d_end && digits[d_end - 1] == 0) {
+ d_end--;
+ }
+ }
+
+ /* output[] is LSB first, reverse to get big-endian */
+ for (size_t i = 0; i < out_idx / 2; i++) {
+ uint8_t tmp = output[i];
+ output[i] = output[out_idx - 1 - i];
+ output[out_idx - 1 - i] = tmp;
+ }
+
+ size_t decoded_len = zeros + out_idx;
+ zend_string *result = zend_string_alloc(decoded_len, 0);
+ char *dst = ZSTR_VAL(result);
+
+ for (size_t i = 0; i < zeros; i++) {
+ *dst++ = 0;
+ }
+ for (size_t i = 0; i < out_idx; i++) {
+ *dst++ = output[i];
+ }
+
+ size_t result_len = (size_t)(dst - ZSTR_VAL(result));
+ ZSTR_LEN(result) = result_len;
+ ZSTR_VAL(result)[result_len] = '\0';
+ efree(output);
+ efree(digits);
+
+ return result;
+}
+
+PHP_FUNCTION(Encoding_base58_encode) {
+ char *data;
+ size_t data_len;
+ zval *variant_obj = NULL;
+ zval *timing_obj = NULL;
+
+ ZEND_PARSE_PARAMETERS_START(1, 3)
+ Z_PARAM_STRING(data, data_len)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_OBJECT_OF_CLASS(variant_obj, encoding_ce_Base58)
+ Z_PARAM_OBJECT_OF_CLASS(timing_obj, encoding_ce_TimingMode)
+ ZEND_PARSE_PARAMETERS_END();
+
+ zend_enum_Encoding_Base58 variant = ZEND_ENUM_Encoding_Base58_Bitcoin;
+ if (variant_obj) {
+ variant = (zend_enum_Encoding_Base58)zend_enum_fetch_case_id(Z_OBJ_P(variant_obj));
+ }
+
+ RETURN_STR(base58_encode_impl(data, data_len, variant));
+}
+
+PHP_FUNCTION(Encoding_base58_decode) {
+ char *data;
+ size_t data_len;
+ zval *variant_obj = NULL;
+ zval *timing_obj = NULL;
+
+ ZEND_PARSE_PARAMETERS_START(1, 3)
+ Z_PARAM_STRING(data, data_len)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_OBJECT_OF_CLASS(variant_obj, encoding_ce_Base58)
+ Z_PARAM_OBJECT_OF_CLASS(timing_obj, encoding_ce_TimingMode)
+ ZEND_PARSE_PARAMETERS_END();
+
+ zend_enum_Encoding_Base58 variant = ZEND_ENUM_Encoding_Base58_Bitcoin;
+ if (variant_obj) {
+ variant = (zend_enum_Encoding_Base58)zend_enum_fetch_case_id(Z_OBJ_P(variant_obj));
+ }
+
+ zend_string *result = base58_decode_impl(data, data_len, variant);
+ if (!result) {
+ return;
+ }
+ RETURN_STR(result);
+}
+
+/* ===================== Base64 ===================== */
+
+static const char base64_standard_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+static const char base64_urlsafe_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
+static const char base64_imap_table[] = "-_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+
+static const uint8_t base64_standard_reverse[256] = {
+ [0 ... 255] = 0xFF,
+ ['A'] = 0, ['B'] = 1, ['C'] = 2, ['D'] = 3, ['E'] = 4, ['F'] = 5, ['G'] = 6, ['H'] = 7,
+ ['I'] = 8, ['J'] = 9, ['K'] = 10, ['L'] = 11, ['M'] = 12, ['N'] = 13, ['O'] = 14, ['P'] = 15,
+ ['Q'] = 16, ['R'] = 17, ['S'] = 18, ['T'] = 19, ['U'] = 20, ['V'] = 21, ['W'] = 22, ['X'] = 23,
+ ['Y'] = 24, ['Z'] = 25, ['a'] = 26, ['b'] = 27, ['c'] = 28, ['d'] = 29, ['e'] = 30, ['f'] = 31,
+ ['g'] = 32, ['h'] = 33, ['i'] = 34, ['j'] = 35, ['k'] = 36, ['l'] = 37, ['m'] = 38, ['n'] = 39,
+ ['o'] = 40, ['p'] = 41, ['q'] = 42, ['r'] = 43, ['s'] = 44, ['t'] = 45, ['u'] = 46, ['v'] = 47,
+ ['w'] = 48, ['x'] = 49, ['y'] = 50, ['z'] = 51, ['0'] = 52, ['1'] = 53, ['2'] = 54, ['3'] = 55,
+ ['4'] = 56, ['5'] = 57, ['6'] = 58, ['7'] = 59, ['8'] = 60, ['9'] = 61, ['+'] = 62, ['/'] = 63
+};
+
+static const uint8_t base64_urlsafe_reverse[256] = {
+ [0 ... 255] = 0xFF,
+ ['A'] = 0, ['B'] = 1, ['C'] = 2, ['D'] = 3, ['E'] = 4, ['F'] = 5, ['G'] = 6, ['H'] = 7,
+ ['I'] = 8, ['J'] = 9, ['K'] = 10, ['L'] = 11, ['M'] = 12, ['N'] = 13, ['O'] = 14, ['P'] = 15,
+ ['Q'] = 16, ['R'] = 17, ['S'] = 18, ['T'] = 19, ['U'] = 20, ['V'] = 21, ['W'] = 22, ['X'] = 23,
+ ['Y'] = 24, ['Z'] = 25, ['a'] = 26, ['b'] = 27, ['c'] = 28, ['d'] = 29, ['e'] = 30, ['f'] = 31,
+ ['g'] = 32, ['h'] = 33, ['i'] = 34, ['j'] = 35, ['k'] = 36, ['l'] = 37, ['m'] = 38, ['n'] = 39,
+ ['o'] = 40, ['p'] = 41, ['q'] = 42, ['r'] = 43, ['s'] = 44, ['t'] = 45, ['u'] = 46, ['v'] = 47,
+ ['w'] = 48, ['x'] = 49, ['y'] = 50, ['z'] = 51, ['0'] = 52, ['1'] = 53, ['2'] = 54, ['3'] = 55,
+ ['4'] = 56, ['5'] = 57, ['6'] = 58, ['7'] = 59, ['8'] = 60, ['9'] = 61, ['-'] = 62, ['_'] = 63
+};
+
+static const uint8_t base64_imap_reverse[256] = {
+ [0 ... 255] = 0xFF,
+ ['-'] = 0, ['_'] = 1,
+ ['A'] = 2, ['B'] = 3, ['C'] = 4, ['D'] = 5, ['E'] = 6, ['F'] = 7,
+ ['G'] = 8, ['H'] = 9, ['I'] = 10, ['J'] = 11, ['K'] = 12, ['L'] = 13, ['O'] = 14, ['P'] = 15,
+ ['Q'] = 16, ['R'] = 17, ['S'] = 18, ['T'] = 19, ['U'] = 20, ['V'] = 21, ['W'] = 22, ['X'] = 23,
+ ['Y'] = 24, ['Z'] = 25, ['a'] = 26, ['b'] = 27, ['c'] = 28, ['d'] = 29, ['e'] = 30, ['f'] = 31,
+ ['g'] = 32, ['h'] = 33, ['i'] = 34, ['j'] = 35, ['k'] = 36, ['l'] = 37, ['m'] = 38, ['n'] = 39,
+ ['o'] = 40, ['p'] = 41, ['q'] = 42, ['r'] = 43, ['s'] = 44, ['t'] = 45, ['u'] = 46, ['v'] = 47,
+ ['w'] = 48, ['x'] = 49, ['y'] = 50, ['z'] = 51, ['0'] = 52, ['1'] = 53, ['2'] = 54, ['3'] = 55,
+ ['4'] = 56, ['5'] = 57, ['6'] = 58, ['7'] = 59, ['8'] = 60, ['9'] = 61, ['N'] = 62, ['/'] = 63
+};
+
+static const uint8_t base64_forgiving_reverse[256] = {
+ [0 ... 255] = 0xFF,
+ ['A'] = 0, ['B'] = 1, ['C'] = 2, ['D'] = 3, ['E'] = 4, ['F'] = 5, ['G'] = 6, ['H'] = 7,
+ ['I'] = 8, ['J'] = 9, ['K'] = 10, ['L'] = 11, ['M'] = 12, ['N'] = 13, ['O'] = 14, ['P'] = 15,
+ ['Q'] = 16, ['R'] = 17, ['S'] = 18, ['T'] = 19, ['U'] = 20, ['V'] = 21, ['W'] = 22, ['X'] = 23,
+ ['Y'] = 24, ['Z'] = 25, ['a'] = 26, ['b'] = 27, ['c'] = 28, ['d'] = 29, ['e'] = 30, ['f'] = 31,
+ ['g'] = 32, ['h'] = 33, ['i'] = 34, ['j'] = 35, ['k'] = 36, ['l'] = 37, ['m'] = 38, ['n'] = 39,
+ ['o'] = 40, ['p'] = 41, ['q'] = 42, ['r'] = 43, ['s'] = 44, ['t'] = 45, ['u'] = 46, ['v'] = 47,
+ ['w'] = 48, ['x'] = 49, ['y'] = 50, ['z'] = 51, ['0'] = 52, ['1'] = 53, ['2'] = 54, ['3'] = 55,
+ ['4'] = 56, ['5'] = 57, ['6'] = 58, ['7'] = 59, ['8'] = 60, ['9'] = 61,
+ ['+'] = 62, ['/'] = 62, ['-'] = 62, ['_'] = 62,
+ ['='] = 63, ['~'] = 63
+};
+
+static inline bool is_base64_whitespace(uint8_t c) {
+ return c == ' ' || c == '\t' || c == '\n' || c == '\r';
+}
+
+static zend_string *base64_encode_impl(const char *data, size_t len, zend_enum_Encoding_Base64 variant, zend_enum_Encoding_PaddingMode padding) {
+ const char *table;
+ bool uses_padding;
+ bool strip_padding;
+
+ switch (variant) {
+ case ZEND_ENUM_Encoding_Base64_Standard:
+ table = base64_standard_table;
+ uses_padding = true;
+ break;
+ case ZEND_ENUM_Encoding_Base64_UrlSafe:
+ table = base64_urlsafe_table;
+ uses_padding = false;
+ break;
+ case ZEND_ENUM_Encoding_Base64_Imap:
+ table = base64_imap_table;
+ uses_padding = true;
+ break;
+ default:
+ table = base64_standard_table;
+ uses_padding = true;
+ break;
+ }
+
+ switch (padding) {
+ case ZEND_ENUM_Encoding_PaddingMode_VariantControlled:
+ strip_padding = !uses_padding;
+ break;
+ case ZEND_ENUM_Encoding_PaddingMode_StripPadding:
+ strip_padding = true;
+ break;
+ case ZEND_ENUM_Encoding_PaddingMode_PreservePadding:
+ strip_padding = false;
+ break;
+ default:
+ strip_padding = false;
+ break;
+ }
+
+ size_t encoded_len = ((len + 2) / 3) * 4;
+ if (strip_padding) {
+ size_t remainder = len % 3;
+ if (remainder) {
+ encoded_len -= 3 - remainder;
+ }
+ }
+
+ zend_string *result = zend_string_alloc(encoded_len, 0);
+ char *dst = ZSTR_VAL(result);
+
+ size_t i = 0;
+ while (i < len) {
+ uint32_t nbits;
+ size_t bytes_left = len - i;
+
+ if (bytes_left >= 3) {
+ nbits = ((uint8_t)data[i] << 16) | ((uint8_t)data[i+1] << 8) | (uint8_t)data[i+2];
+ *dst++ = table[(nbits >> 18) & 0x3F];
+ *dst++ = table[(nbits >> 12) & 0x3F];
+ *dst++ = table[(nbits >> 6) & 0x3F];
+ *dst++ = table[nbits & 0x3F];
+ i += 3;
+ } else {
+ if (bytes_left == 2) {
+ nbits = ((uint8_t)data[i] << 16) | ((uint8_t)data[i+1] << 8);
+ } else {
+ nbits = (uint8_t)data[i] << 16;
+ }
+ *dst++ = table[(nbits >> 18) & 0x3F];
+ *dst++ = table[(nbits >> 12) & 0x3F];
+ if (bytes_left == 2) {
+ *dst++ = table[(nbits >> 6) & 0x3F];
+ }
+ if (!strip_padding) {
+ *dst++ = '=';
+ if (bytes_left == 1) {
+ *dst++ = '=';
+ }
+ }
+ i += bytes_left;
+ }
+ }
+
+ ZSTR_LEN(result) = dst - ZSTR_VAL(result);
+ ZSTR_VAL(result)[dst - ZSTR_VAL(result)] = '\0';
+ return result;
+}
+
+static zend_string *base64_decode_impl(const char *data, size_t len, zend_enum_Encoding_Base64 variant, zend_enum_Encoding_DecodingMode mode) {
+ const uint8_t *reverse_table;
+
+ if (mode == ZEND_ENUM_Encoding_DecodingMode_Forgiving) {
+ reverse_table = base64_forgiving_reverse;
+ } else {
+ switch (variant) {
+ case ZEND_ENUM_Encoding_Base64_Standard:
+ reverse_table = base64_standard_reverse;
+ break;
+ case ZEND_ENUM_Encoding_Base64_UrlSafe:
+ reverse_table = base64_urlsafe_reverse;
+ break;
+ case ZEND_ENUM_Encoding_Base64_Imap:
+ reverse_table = base64_imap_reverse;
+ break;
+ default:
+ reverse_table = base64_standard_reverse;
+ break;
+ }
+ }
+
+ bool forgiving = (mode == ZEND_ENUM_Encoding_DecodingMode_Forgiving);
+
+ /* Count content and padding */
+ size_t content_len = 0;
+ size_t padding_count = 0;
+ for (size_t i = 0; i < len; i++) {
+ uint8_t c = (uint8_t)data[i];
+ if (is_base64_whitespace(c)) {
+ continue;
+ }
+ if (c == '=') {
+ padding_count++;
+ continue;
+ }
+ content_len++;
+ }
+
+ /* Validate padding */
+ if (forgiving) {
+ if (padding_count > 2) {
+ THROW_UNABLE_TO_DECODE("Invalid padding length");
+ return NULL;
+ }
+ } else {
+ size_t remainder = content_len % 4;
+ if (remainder == 0) {
+ if (padding_count != 0) {
+ THROW_UNABLE_TO_DECODE("Invalid padding length");
+ return NULL;
+ }
+ } else if (remainder == 2) {
+ if (padding_count != 2) {
+ THROW_UNABLE_TO_DECODE("Invalid padding length");
+ return NULL;
+ }
+ } else if (remainder == 3) {
+ if (padding_count != 1) {
+ THROW_UNABLE_TO_DECODE("Invalid padding length");
+ return NULL;
+ }
+ } else if (remainder == 1) {
+ THROW_UNABLE_TO_DECODE("Invalid Base64 data");
+ return NULL;
+ }
+ }
+
+ size_t out_len = (content_len * 3) / 4;
+ if (padding_count) {
+ out_len -= padding_count;
+ }
+ zend_string *result = zend_string_alloc(out_len, 0);
+ char *dst = ZSTR_VAL(result);
+ size_t dst_idx = 0;
+
+ uint32_t nbits = 0;
+ int bits = 0;
+
+ for (size_t i = 0; i < len; i++) {
+ uint8_t c = (uint8_t)data[i];
+
+ if (is_base64_whitespace(c)) {
+ continue;
+ }
+
+ if (c == '=') {
+ continue;
+ }
+
+ uint8_t val = reverse_table[c];
+
+ if (val == 0xFF) {
+ zend_string_efree(result);
+ THROW_UNABLE_TO_DECODE("Invalid Base64 character");
+ return NULL;
+ }
+
+ nbits = (nbits << 6) | val;
+ bits += 6;
+
+ while (bits >= 8) {
+ bits -= 8;
+ if (dst_idx < out_len) {
+ dst[dst_idx++] = (char)((nbits >> bits) & 0xFF);
+ }
+ }
+ }
+
+ ZSTR_LEN(result) = dst_idx;
+ ZSTR_VAL(result)[dst_idx] = '\0';
+ return result;
+}
+
+PHP_FUNCTION(Encoding_base64_encode) {
+ char *data;
+ size_t data_len;
+ zval *variant_obj = NULL;
+ zval *padding_obj = NULL;
+ zval *timing_obj = NULL;
+
+ ZEND_PARSE_PARAMETERS_START(1, 4)
+ Z_PARAM_STRING(data, data_len)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_OBJECT_OF_CLASS(variant_obj, encoding_ce_Base64)
+ Z_PARAM_OBJECT_OF_CLASS(padding_obj, encoding_ce_PaddingMode)
+ Z_PARAM_OBJECT_OF_CLASS(timing_obj, encoding_ce_TimingMode)
+ ZEND_PARSE_PARAMETERS_END();
+
+ zend_enum_Encoding_Base64 variant = ZEND_ENUM_Encoding_Base64_Standard;
+ if (variant_obj) {
+ variant = (zend_enum_Encoding_Base64)zend_enum_fetch_case_id(Z_OBJ_P(variant_obj));
+ }
+
+ zend_enum_Encoding_PaddingMode padding = ZEND_ENUM_Encoding_PaddingMode_VariantControlled;
+ if (padding_obj) {
+ padding = (zend_enum_Encoding_PaddingMode)zend_enum_fetch_case_id(Z_OBJ_P(padding_obj));
+ }
+
+ RETURN_STR(base64_encode_impl(data, data_len, variant, padding));
+}
+
+PHP_FUNCTION(Encoding_base64_decode) {
+ char *data;
+ size_t data_len;
+ zval *variant_obj = NULL;
+ zval *decoding_obj = NULL;
+ zval *timing_obj = NULL;
+
+ ZEND_PARSE_PARAMETERS_START(1, 4)
+ Z_PARAM_STRING(data, data_len)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_OBJECT_OF_CLASS(variant_obj, encoding_ce_Base64)
+ Z_PARAM_OBJECT_OF_CLASS(decoding_obj, encoding_ce_DecodingMode)
+ Z_PARAM_OBJECT_OF_CLASS(timing_obj, encoding_ce_TimingMode)
+ ZEND_PARSE_PARAMETERS_END();
+
+ zend_enum_Encoding_Base64 variant = ZEND_ENUM_Encoding_Base64_Standard;
+ if (variant_obj) {
+ variant = (zend_enum_Encoding_Base64)zend_enum_fetch_case_id(Z_OBJ_P(variant_obj));
+ }
+
+ zend_enum_Encoding_DecodingMode mode = ZEND_ENUM_Encoding_DecodingMode_Strict;
+ if (decoding_obj) {
+ mode = (zend_enum_Encoding_DecodingMode)zend_enum_fetch_case_id(Z_OBJ_P(decoding_obj));
+ }
+
+ zend_string *result = base64_decode_impl(data, data_len, variant, mode);
+ if (!result) {
+ return;
+ }
+ RETURN_STR(result);
+}
+
+/* ===================== Base85 ===================== */
+
+static const char base85_adobe_table[] = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstu";
+static const char base85_z85_table[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#";
+static const char base85_git_table[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ!@$%^&*()-=+;:<>|~.#";
+
+static const uint8_t base85_adobe_reverse[256] = {
+ [0 ... 255] = 0xFF,
+ ['!'] = 0, ['"'] = 1, ['#'] = 2, ['$'] = 3, ['%'] = 4, ['&'] = 5, ['\''] = 6, ['('] = 7,
+ [')'] = 8, ['*'] = 9, ['+'] = 10, [','] = 11, ['-'] = 12, ['.'] = 13, ['/'] = 14, ['0'] = 15,
+ ['1'] = 16, ['2'] = 17, ['3'] = 18, ['4'] = 19, ['5'] = 20, ['6'] = 21, ['7'] = 22, ['8'] = 23,
+ ['9'] = 24, [':'] = 25, [';'] = 26, ['<'] = 27, ['='] = 28, ['>'] = 29, ['?'] = 30, ['@'] = 31,
+ ['A'] = 32, ['B'] = 33, ['C'] = 34, ['D'] = 35, ['E'] = 36, ['F'] = 37, ['G'] = 38, ['H'] = 39,
+ ['I'] = 40, ['J'] = 41, ['K'] = 42, ['L'] = 43, ['M'] = 44, ['N'] = 45, ['O'] = 46, ['P'] = 47,
+ ['Q'] = 48, ['R'] = 49, ['S'] = 50, ['T'] = 51, ['U'] = 52, ['V'] = 53, ['W'] = 54, ['X'] = 55,
+ ['Y'] = 56, ['Z'] = 57, ['['] = 58, ['\\'] = 59, [']'] = 60, ['^'] = 61, ['_'] = 62, ['`'] = 63,
+ ['a'] = 64, ['b'] = 65, ['c'] = 66, ['d'] = 67, ['e'] = 68, ['f'] = 69, ['g'] = 70, ['h'] = 71,
+ ['i'] = 72, ['j'] = 73, ['k'] = 74, ['l'] = 75, ['m'] = 76, ['n'] = 77, ['o'] = 78, ['p'] = 79,
+ ['q'] = 80, ['r'] = 81, ['s'] = 82, ['t'] = 83, ['u'] = 84
+};
+
+static const uint8_t base85_z85_reverse[256] = {
+ [0 ... 255] = 0xFF,
+ ['0'] = 0, ['1'] = 1, ['2'] = 2, ['3'] = 3, ['4'] = 4, ['5'] = 5, ['6'] = 6, ['7'] = 7,
+ ['8'] = 8, ['9'] = 9, ['a'] = 10, ['b'] = 11, ['c'] = 12, ['d'] = 13, ['e'] = 14, ['f'] = 15,
+ ['g'] = 16, ['h'] = 17, ['i'] = 18, ['j'] = 19, ['k'] = 20, ['l'] = 21, ['m'] = 22, ['n'] = 23,
+ ['o'] = 24, ['p'] = 25, ['q'] = 26, ['r'] = 27, ['s'] = 28, ['t'] = 29, ['u'] = 30, ['v'] = 31,
+ ['w'] = 32, ['x'] = 33, ['y'] = 34, ['z'] = 35, ['A'] = 36, ['B'] = 37, ['C'] = 38, ['D'] = 39,
+ ['E'] = 40, ['F'] = 41, ['G'] = 42, ['H'] = 43, ['I'] = 44, ['J'] = 45, ['K'] = 46, ['L'] = 47,
+ ['M'] = 48, ['N'] = 49, ['O'] = 50, ['P'] = 51, ['Q'] = 52, ['R'] = 53, ['S'] = 54, ['T'] = 55,
+ ['U'] = 56, ['V'] = 57, ['W'] = 58, ['X'] = 59, ['Y'] = 60, ['Z'] = 61, ['.'] = 62, ['-'] = 63,
+ [':'] = 64, ['+'] = 65, ['='] = 66, ['^'] = 67, ['!'] = 68, ['/'] = 69, ['*'] = 70, ['?'] = 71,
+ ['&'] = 72, ['<'] = 73, ['>'] = 74, ['('] = 75, [')'] = 76, ['['] = 77, [']'] = 78, ['{'] = 79,
+ ['}'] = 80, ['@'] = 81, ['%'] = 82, ['$'] = 83, ['#'] = 84
+};
+
+static const uint8_t base85_git_reverse[256] = {
+ [0 ... 255] = 0xFF,
+ ['0'] = 0, ['1'] = 1, ['2'] = 2, ['3'] = 3, ['4'] = 4, ['5'] = 5, ['6'] = 6, ['7'] = 7,
+ ['8'] = 8, ['9'] = 9,
+ ['A'] = 10, ['B'] = 11, ['C'] = 12, ['D'] = 13, ['E'] = 14, ['F'] = 15, ['G'] = 16, ['H'] = 17,
+ ['I'] = 18, ['J'] = 19, ['K'] = 20, ['L'] = 21, ['M'] = 22, ['N'] = 23, ['O'] = 24, ['P'] = 25,
+ ['Q'] = 26, ['R'] = 27, ['S'] = 28, ['T'] = 29, ['U'] = 30, ['V'] = 31, ['W'] = 32, ['X'] = 33,
+ ['Y'] = 34, ['Z'] = 35,
+ ['!'] = 36, ['@'] = 37, ['$'] = 38, ['%'] = 39, ['^'] = 40, ['&'] = 41,
+ ['*'] = 42, ['('] = 43, [')'] = 44, ['-'] = 45, ['='] = 46, ['+'] = 47,
+ [';'] = 48, [':'] = 49, ['<'] = 50, ['>'] = 51, ['|'] = 52, ['~'] = 53, ['.'] = 54, ['#'] = 55
+};
+
+static zend_string *base85_encode_impl(const char *data, size_t len, zend_enum_Encoding_Base85 variant, zend_enum_Encoding_PaddingMode padding) {
+ const char *table;
+ bool uses_padding;
+
+ switch (variant) {
+ case ZEND_ENUM_Encoding_Base85_Adobe:
+ table = base85_adobe_table;
+ uses_padding = true;
+ break;
+ case ZEND_ENUM_Encoding_Base85_Z85:
+ table = base85_z85_table;
+ uses_padding = false;
+ break;
+ case ZEND_ENUM_Encoding_Base85_Git:
+ table = base85_git_table;
+ uses_padding = false;
+ break;
+ default:
+ table = base85_adobe_table;
+ uses_padding = true;
+ break;
+ }
+
+ if (padding == ZEND_ENUM_Encoding_PaddingMode_PreservePadding && !uses_padding) {
+ zend_value_error("PaddingMode::PreservePadding is not supported for variant %s",
+ variant == ZEND_ENUM_Encoding_Base85_Z85 ? "Base85::Z85" : "Base85::Git");
+ return NULL;
+ }
+
+ if (padding == ZEND_ENUM_Encoding_PaddingMode_StripPadding && !uses_padding) {
+ zend_value_error("PaddingMode::StripPadding is not supported for variant %s",
+ variant == ZEND_ENUM_Encoding_Base85_Z85 ? "Base85::Z85" : "Base85::Git");
+ return NULL;
+ }
+
+ bool strip_padding = (padding == ZEND_ENUM_Encoding_PaddingMode_StripPadding);
+ bool add_markers = uses_padding && !strip_padding;
+
+ if (variant == ZEND_ENUM_Encoding_Base85_Z85 && len % 4 != 0) {
+ zend_value_error("Z85 encoding requires input length to be a multiple of 4");
+ return NULL;
+ }
+
+ /* Calculate total output length */
+ size_t full_blocks = len / 4;
+ size_t remainder = len % 4;
+ size_t total_chars = full_blocks * 5;
+ if (remainder) {
+ if (strip_padding) {
+ total_chars += remainder;
+ } else {
+ total_chars += remainder + 1;
+ }
+ }
+ if (add_markers) total_chars += 4;
+
+ zend_string *result = zend_string_alloc(total_chars, 0);
+ char *dst = ZSTR_VAL(result);
+
+ if (add_markers) {
+ *dst++ = '<';
+ *dst++ = '~';
+ }
+
+ for (size_t i = 0; i < len; i += 4) {
+ size_t bytes_left = len - i;
+ size_t bytes_in_block = bytes_left < 4 ? bytes_left : 4;
+
+ uint32_t n = 0;
+ for (size_t j = 0; j < bytes_in_block; j++) {
+ n = (n << 8) | (uint8_t)data[i + j];
+ }
+
+ if (bytes_in_block == 4) {
+ /* Full block: encode as 5 base-85 digits, MSD first */
+ if (n == 0 && variant == ZEND_ENUM_Encoding_Base85_Adobe) {
+ /* Adobe: all-zero block encoded as single 'z' */
+ *dst++ = 'z';
+ } else {
+ uint32_t t = n;
+ uint8_t digits[5];
+ for (int d = 4; d >= 0; d--) {
+ digits[d] = (uint8_t)(t % 85);
+ t /= 85;
+ }
+ for (int d = 0; d < 5; d++) {
+ *dst++ = table[digits[d]];
+ }
+ }
+ } else {
+ /* Partial block: left-shift to align, then divide by 85 */
+ n <<= (4 - bytes_in_block) * 8;
+
+ if (strip_padding) {
+ /* Output only bytes_in_block chars */
+ uint32_t t = n;
+ uint8_t digits[5];
+ for (int d = 4; d >= 0; d--) {
+ digits[d] = (uint8_t)(t % 85);
+ t /= 85;
+ }
+ for (int d = 0; d < (int)bytes_in_block; d++) {
+ *dst++ = table[digits[d]];
+ }
+ } else {
+ /* Output bytes_in_block + 1 chars */
+ uint32_t t = n;
+ uint8_t digits[5];
+ for (int d = 4; d >= 0; d--) {
+ digits[d] = (uint8_t)(t % 85);
+ t /= 85;
+ }
+ for (int d = 0; d <= (int)bytes_in_block; d++) {
+ *dst++ = table[digits[d]];
+ }
+ }
+ }
+ }
+
+ if (add_markers) {
+ *dst++ = '~';
+ *dst++ = '>';
+ }
+
+ ZSTR_LEN(result) = dst - ZSTR_VAL(result);
+ ZSTR_VAL(result)[ZSTR_LEN(result)] = '\0';
+ return result;
+}
+
+static zend_string *base85_decode_impl(const char *data, size_t len, zend_enum_Encoding_Base85 variant, zend_enum_Encoding_DecodingMode mode) {
+ const uint8_t *reverse_table;
+
+ switch (variant) {
+ case ZEND_ENUM_Encoding_Base85_Adobe:
+ reverse_table = base85_adobe_reverse;
+ break;
+ case ZEND_ENUM_Encoding_Base85_Z85:
+ reverse_table = base85_z85_reverse;
+ break;
+ case ZEND_ENUM_Encoding_Base85_Git:
+ reverse_table = base85_git_reverse;
+ break;
+ default:
+ reverse_table = base85_adobe_reverse;
+ break;
+ }
+
+ bool forgiving = (mode == ZEND_ENUM_Encoding_DecodingMode_Forgiving);
+
+ /* For Adobe variant, strip <~ and ~> markers if present */
+ if (variant == ZEND_ENUM_Encoding_Base85_Adobe && len >= 4 && data[0] == '<' && data[1] == '~') {
+ if (data[len - 2] == '~' && data[len - 1] == '>') {
+ data += 2;
+ len -= 2;
+ } else if (forgiving) {
+ data += 2;
+ len -= 2;
+ } else {
+ THROW_UNABLE_TO_DECODE("Invalid Adobe Base85 format: missing closing marker");
+ return NULL;
+ }
+ }
+
+ /* Collect non-whitespace characters */
+ uint8_t *content = emalloc(len);
+ size_t content_len = 0;
+ for (size_t i = 0; i < len; i++) {
+ uint8_t c = (uint8_t)data[i];
+ if (is_base64_whitespace(c)) {
+ continue;
+ }
+ content[content_len++] = c;
+ }
+
+ /* Z85 requires multiple of 5 */
+ if (variant == ZEND_ENUM_Encoding_Base85_Z85 && content_len % 5 != 0) {
+ efree(content);
+ THROW_UNABLE_TO_DECODE("Z85 encoded data must have length multiple of 5");
+ return NULL;
+ }
+
+ /* Calculate output length and allocate */
+ size_t full_blocks = 0;
+ size_t z_blocks = 0;
+ size_t remainder = content_len;
+
+ for (size_t i = 0; i < remainder; ) {
+ if (content[i] == 'z' && variant == ZEND_ENUM_Encoding_Base85_Adobe) {
+ z_blocks++;
+ i++;
+ remainder--;
+ } else {
+ if (remainder >= 5) {
+ full_blocks++;
+ i += 5;
+ remainder -= 5;
+ } else {
+ break;
+ }
+ }
+ }
+
+ size_t out_len = full_blocks * 4 + z_blocks * 4;
+ if (remainder) {
+ out_len += remainder - 1;
+ }
+
+ zend_string *result = zend_string_alloc(out_len, 0);
+ char *dst = ZSTR_VAL(result);
+
+ /* Decode content */
+ size_t i = 0;
+ while (i < content_len) {
+ if (content[i] == 'z' && variant == ZEND_ENUM_Encoding_Base85_Adobe) {
+ /* Adobe 'z' shortcut: 4 zero bytes */
+ *dst++ = '\0';
+ *dst++ = '\0';
+ *dst++ = '\0';
+ *dst++ = '\0';
+ i++;
+ continue;
+ }
+
+ /* Count characters in this block (non-'z') */
+ size_t block_start = i;
+ size_t chars_in_block = 0;
+ while (i < content_len && chars_in_block < 5) {
+ if (content[i] == 'z' && variant == ZEND_ENUM_Encoding_Base85_Adobe) {
+ break; /* 'z' starts a new block */
+ }
+ i++;
+ chars_in_block++;
+ }
+
+ /* Decode this block */
+ uint32_t n = 0;
+ size_t pad_count = 5 - chars_in_block;
+ for (size_t j = block_start; j < i; j++) {
+ uint8_t val = reverse_table[content[j]];
+ if (val == 0xFF || val >= 85) {
+ efree(content);
+ zend_string_efree(result);
+ THROW_UNABLE_TO_DECODE("Invalid Base85 character");
+ return NULL;
+ }
+ n = n * 85 + val;
+ }
+ /* Pad with 84 ('u') for partial blocks */
+ for (size_t j = 0; j < pad_count; j++) {
+ n = n * 85 + 84;
+ }
+
+ /* Extract bytes */
+ size_t bytes_to_output = (chars_in_block == 5) ? 4 : (chars_in_block - 1);
+ for (size_t j = 0; j < bytes_to_output; j++) {
+ *dst++ = (char)((n >> ((4 - j - 1) * 8)) & 0xFF);
+ }
+ }
+
+ efree(content);
+ ZSTR_LEN(result) = dst - ZSTR_VAL(result);
+ ZSTR_VAL(result)[ZSTR_LEN(result)] = '\0';
+ return result;
+}
+
+PHP_FUNCTION(Encoding_base85_encode) {
+ char *data;
+ size_t data_len;
+ zval *variant_obj;
+ zval *padding_obj = NULL;
+ zval *timing_obj = NULL;
+
+ ZEND_PARSE_PARAMETERS_START(2, 4)
+ Z_PARAM_STRING(data, data_len)
+ Z_PARAM_OBJECT_OF_CLASS(variant_obj, encoding_ce_Base85)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_OBJECT_OF_CLASS(padding_obj, encoding_ce_PaddingMode)
+ Z_PARAM_OBJECT_OF_CLASS(timing_obj, encoding_ce_TimingMode)
+ ZEND_PARSE_PARAMETERS_END();
+
+ zend_enum_Encoding_Base85 variant = (zend_enum_Encoding_Base85)zend_enum_fetch_case_id(Z_OBJ_P(variant_obj));
+
+ zend_enum_Encoding_PaddingMode padding = ZEND_ENUM_Encoding_PaddingMode_VariantControlled;
+ if (padding_obj) {
+ padding = (zend_enum_Encoding_PaddingMode)zend_enum_fetch_case_id(Z_OBJ_P(padding_obj));
+ }
+
+ zend_string *result = base85_encode_impl(data, data_len, variant, padding);
+ if (!result) {
+ return;
+ }
+ RETURN_STR(result);
+}
+
+PHP_FUNCTION(Encoding_base85_decode) {
+ char *data;
+ size_t data_len;
+ zval *variant_obj;
+ zval *decoding_obj = NULL;
+ zval *timing_obj = NULL;
+
+ ZEND_PARSE_PARAMETERS_START(2, 4)
+ Z_PARAM_STRING(data, data_len)
+ Z_PARAM_OBJECT_OF_CLASS(variant_obj, encoding_ce_Base85)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_OBJECT_OF_CLASS(decoding_obj, encoding_ce_DecodingMode)
+ Z_PARAM_OBJECT_OF_CLASS(timing_obj, encoding_ce_TimingMode)
+ ZEND_PARSE_PARAMETERS_END();
+
+ zend_enum_Encoding_Base85 variant = (zend_enum_Encoding_Base85)zend_enum_fetch_case_id(Z_OBJ_P(variant_obj));
+
+ zend_enum_Encoding_DecodingMode mode = ZEND_ENUM_Encoding_DecodingMode_Strict;
+ if (decoding_obj) {
+ mode = (zend_enum_Encoding_DecodingMode)zend_enum_fetch_case_id(Z_OBJ_P(decoding_obj));
+ }
+
+ zend_string *result = base85_decode_impl(data, data_len, variant, mode);
+ if (!result) {
+ return;
+ }
+ RETURN_STR(result);
+}
+
+/* ===================== Module Registration ===================== */
+
+static PHP_MINIT_FUNCTION(encoding)
+{
+ encoding_ce_EncodingError = register_class_Encoding_EncodingError(zend_ce_error);
+ encoding_ce_EncodingException = register_class_Encoding_EncodingException(zend_ce_exception);
+ encoding_ce_UnableToDecodeException = register_class_Encoding_UnableToDecodeException(encoding_ce_EncodingException);
+ encoding_ce_UnableToEncodeException = register_class_Encoding_UnableToEncodeException(encoding_ce_EncodingException);
+
+ encoding_ce_Base16 = register_class_Encoding_Base16();
+ encoding_ce_Base32 = register_class_Encoding_Base32();
+ encoding_ce_Base58 = register_class_Encoding_Base58();
+ encoding_ce_Base64 = register_class_Encoding_Base64();
+ encoding_ce_Base85 = register_class_Encoding_Base85();
+ encoding_ce_PaddingMode = register_class_Encoding_PaddingMode();
+ encoding_ce_DecodingMode = register_class_Encoding_DecodingMode();
+ encoding_ce_TimingMode = register_class_Encoding_TimingMode();
+
+ return SUCCESS;
+}
+
+zend_module_entry encoding_module_entry = {
+ STANDARD_MODULE_HEADER,
+ "encoding",
+ ext_functions,
+ PHP_MINIT(encoding),
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ PHP_VERSION,
+ STANDARD_MODULE_PROPERTIES
+};
+
+#ifdef COMPILE_DL_ENCODING
+ZEND_GET_MODULE(encoding)
+#endif
diff --git a/ext/encoding/encoding.stub.php b/ext/encoding/encoding.stub.php
new file mode 100644
index 000000000000..c8714548f975
--- /dev/null
+++ b/ext/encoding/encoding.stub.php
@@ -0,0 +1,140 @@
+
+--EXPECT--
+string(12) "Hello world!"
+string(12) "Hello world!"
diff --git a/ext/encoding/tests/base16_decode_exceptions.phpt b/ext/encoding/tests/base16_decode_exceptions.phpt
new file mode 100644
index 000000000000..1f1222020481
--- /dev/null
+++ b/ext/encoding/tests/base16_decode_exceptions.phpt
@@ -0,0 +1,33 @@
+--TEST--
+Base16 decode strict exceptions
+--EXTENSIONS--
+encoding
+--FILE--
+
+--EXPECTF--
+Encoding\UnableToDecodeException
+Encoding\UnableToDecodeException
diff --git a/ext/encoding/tests/base16_decode_forgiving.phpt b/ext/encoding/tests/base16_decode_forgiving.phpt
new file mode 100644
index 000000000000..5d7b5e72e35e
--- /dev/null
+++ b/ext/encoding/tests/base16_decode_forgiving.phpt
@@ -0,0 +1,27 @@
+--TEST--
+Base16 decode forgiving mode
+--EXTENSIONS--
+encoding
+--FILE--
+
+--EXPECT--
+string(12) "Hello world!"
+string(12) "Hello world!"
+string(12) "Hello world!"
diff --git a/ext/encoding/tests/base16_encode.phpt b/ext/encoding/tests/base16_encode.phpt
new file mode 100644
index 000000000000..d96d63263820
--- /dev/null
+++ b/ext/encoding/tests/base16_encode.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Base16 encode
+--EXTENSIONS--
+encoding
+--FILE--
+
+--EXPECT--
+48656C6C6F20776F726C6421
+48656c6c6f20776f726c6421
diff --git a/ext/encoding/tests/base32_decode.phpt b/ext/encoding/tests/base32_decode.phpt
new file mode 100644
index 000000000000..3ed1300d6094
--- /dev/null
+++ b/ext/encoding/tests/base32_decode.phpt
@@ -0,0 +1,22 @@
+--TEST--
+Base32 decode
+--EXTENSIONS--
+encoding
+--FILE--
+
+--EXPECT--
+string(12) "Hello world!"
+string(12) "Hello world!"
diff --git a/ext/encoding/tests/base32_decode_exceptions.phpt b/ext/encoding/tests/base32_decode_exceptions.phpt
new file mode 100644
index 000000000000..4728c518971c
--- /dev/null
+++ b/ext/encoding/tests/base32_decode_exceptions.phpt
@@ -0,0 +1,34 @@
+--TEST--
+Base32 decode strict exceptions
+--EXTENSIONS--
+encoding
+--FILE--
+
+--EXPECTF--
+Encoding\UnableToDecodeException
+string(3) "Big"
+Encoding\UnableToDecodeException
diff --git a/ext/encoding/tests/base32_decode_forgiving.phpt b/ext/encoding/tests/base32_decode_forgiving.phpt
new file mode 100644
index 000000000000..105025005fab
--- /dev/null
+++ b/ext/encoding/tests/base32_decode_forgiving.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Base32 decode forgiving mode
+--EXTENSIONS--
+encoding
+--FILE--
+
+--EXPECT--
+string(12) "Hello world!"
diff --git a/ext/encoding/tests/base32_encode.phpt b/ext/encoding/tests/base32_encode.phpt
new file mode 100644
index 000000000000..6c4425082f6e
--- /dev/null
+++ b/ext/encoding/tests/base32_encode.phpt
@@ -0,0 +1,32 @@
+--TEST--
+Base32 encode
+--EXTENSIONS--
+encoding
+--FILE--
+
+--EXPECT--
+JBSWY3DPEB3W64TMMQQQ====
+JBSWY3DPEB3W64TMMQQQ====
+JBSWY3DPEB3W64TMMQQQ
+91JPRV3F41VPYWKCCGGG
diff --git a/ext/encoding/tests/base32_encode_value_error.phpt b/ext/encoding/tests/base32_encode_value_error.phpt
new file mode 100644
index 000000000000..dfa9626827b6
--- /dev/null
+++ b/ext/encoding/tests/base32_encode_value_error.phpt
@@ -0,0 +1,24 @@
+--TEST--
+Base32 encode ValueError
+--EXTENSIONS--
+encoding
+--FILE--
+
+--EXPECTF--
+ValueError
diff --git a/ext/encoding/tests/base58_decode.phpt b/ext/encoding/tests/base58_decode.phpt
new file mode 100644
index 000000000000..8bf4786195d7
--- /dev/null
+++ b/ext/encoding/tests/base58_decode.phpt
@@ -0,0 +1,22 @@
+--TEST--
+Base58 decode
+--EXTENSIONS--
+encoding
+--FILE--
+
+--EXPECT--
+string(12) "Hello world!"
+string(12) "Hello world!"
diff --git a/ext/encoding/tests/base58_decode_exception.phpt b/ext/encoding/tests/base58_decode_exception.phpt
new file mode 100644
index 000000000000..9fc59745f95c
--- /dev/null
+++ b/ext/encoding/tests/base58_decode_exception.phpt
@@ -0,0 +1,39 @@
+--TEST--
+Base58 decode invalid character exception
+--EXTENSIONS--
+encoding
+--FILE--
+
+--EXPECTF--
+Encoding\UnableToDecodeException
+Encoding\UnableToDecodeException
+Encoding\UnableToDecodeException
diff --git a/ext/encoding/tests/base58_encode.phpt b/ext/encoding/tests/base58_encode.phpt
new file mode 100644
index 000000000000..9a6c5d8bdcb6
--- /dev/null
+++ b/ext/encoding/tests/base58_encode.phpt
@@ -0,0 +1,27 @@
+--TEST--
+Base58 encode
+--EXTENSIONS--
+encoding
+--FILE--
+
+--EXPECT--
+2NEpo7TZRhna7vSvL
+2NEpo7TZRhna7vSvL
+2nePN7syqGMz7VrVk
diff --git a/ext/encoding/tests/base58_msporny_examples.phpt b/ext/encoding/tests/base58_msporny_examples.phpt
new file mode 100644
index 000000000000..7e200019dedd
--- /dev/null
+++ b/ext/encoding/tests/base58_msporny_examples.phpt
@@ -0,0 +1,37 @@
+--TEST--
+Base58 draft-msporny-base58 test vectors
+--EXTENSIONS--
+encoding
+--FILE--
+
+--EXPECT--
+2NEpo7TZRRrLZSi2U
+USm3fpXnKG5EUBx2ndxBDMPVciP5hGey2Jh4NDv6gmeo1LkMeiKrLJUUBk6Z
+11233QC4
+bool(true)
+bool(true)
+bool(true)
diff --git a/ext/encoding/tests/base64_decode_exception.phpt b/ext/encoding/tests/base64_decode_exception.phpt
new file mode 100644
index 000000000000..2b2f50d6febf
--- /dev/null
+++ b/ext/encoding/tests/base64_decode_exception.phpt
@@ -0,0 +1,21 @@
+--TEST--
+Base64 decode strict exception
+--EXTENSIONS--
+encoding
+--FILE--
+
+--EXPECTF--
+Encoding\UnableToDecodeException
diff --git a/ext/encoding/tests/base64_decode_forgiving.phpt b/ext/encoding/tests/base64_decode_forgiving.phpt
new file mode 100644
index 000000000000..d225e623876b
--- /dev/null
+++ b/ext/encoding/tests/base64_decode_forgiving.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Base64 decode forgiving mode
+--EXTENSIONS--
+encoding
+--FILE--
+
+--EXPECT--
+string(25) "This is an encoded string"
diff --git a/ext/encoding/tests/base64_encode.phpt b/ext/encoding/tests/base64_encode.phpt
new file mode 100644
index 000000000000..9125692dda47
--- /dev/null
+++ b/ext/encoding/tests/base64_encode.phpt
@@ -0,0 +1,37 @@
+--TEST--
+Base64 encode
+--EXTENSIONS--
+encoding
+--FILE--
+
+--EXPECT--
+VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw==
+VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw
+//8=
+__8
+//8
diff --git a/ext/encoding/tests/base85_decode.phpt b/ext/encoding/tests/base85_decode.phpt
new file mode 100644
index 000000000000..5fd1ab5dd275
--- /dev/null
+++ b/ext/encoding/tests/base85_decode.phpt
@@ -0,0 +1,18 @@
+--TEST--
+Base85 decode
+--EXTENSIONS--
+encoding
+--FILE--
+
+--EXPECT--
+string(12) "Hello world!"
diff --git a/ext/encoding/tests/base85_decode_forgiving.phpt b/ext/encoding/tests/base85_decode_forgiving.phpt
new file mode 100644
index 000000000000..ef6198801f25
--- /dev/null
+++ b/ext/encoding/tests/base85_decode_forgiving.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Base85 decode forgiving mode
+--EXTENSIONS--
+encoding
+--FILE--
+
+--EXPECT--
+string(12) "Hello world!"
diff --git a/ext/encoding/tests/base85_decode_wrong_variant.phpt b/ext/encoding/tests/base85_decode_wrong_variant.phpt
new file mode 100644
index 000000000000..c218003098cb
--- /dev/null
+++ b/ext/encoding/tests/base85_decode_wrong_variant.phpt
@@ -0,0 +1,24 @@
+--TEST--
+Base85 decode wrong variant exception
+--EXTENSIONS--
+encoding
+--FILE--
+
+--EXPECTF--
+Encoding\UnableToDecodeException
diff --git a/ext/encoding/tests/base85_encode.phpt b/ext/encoding/tests/base85_encode.phpt
new file mode 100644
index 000000000000..054b58f17729
--- /dev/null
+++ b/ext/encoding/tests/base85_encode.phpt
@@ -0,0 +1,24 @@
+--TEST--
+Base85 encode
+--EXTENSIONS--
+encoding
+--FILE--
+
+--EXPECT--
+<~87cURD]j7BEbo80~>
+87cURD]j7BEbo80
diff --git a/ext/encoding/tests/base85_encode_value_error.phpt b/ext/encoding/tests/base85_encode_value_error.phpt
new file mode 100644
index 000000000000..cd912522c818
--- /dev/null
+++ b/ext/encoding/tests/base85_encode_value_error.phpt
@@ -0,0 +1,24 @@
+--TEST--
+Base85 encode ValueError
+--EXTENSIONS--
+encoding
+--FILE--
+
+--EXPECTF--
+ValueError
diff --git a/ext/encoding/tests/base85_wikipedia_examples.phpt b/ext/encoding/tests/base85_wikipedia_examples.phpt
new file mode 100644
index 000000000000..0a38cebc5543
--- /dev/null
+++ b/ext/encoding/tests/base85_wikipedia_examples.phpt
@@ -0,0 +1,57 @@
+--TEST--
+Base85 Wikipedia examples (Adobe Ascii85)
+--EXTENSIONS--
+encoding
+--FILE--
+ "9jqo^"
+echo base85_encode("Man ", variant: Base85::Adobe, paddingMode: PaddingMode::StripPadding);
+echo "\n";
+
+// Example from Wikipedia: "sure" -> "F*2M7"
+echo base85_encode("sure", variant: Base85::Adobe, paddingMode: PaddingMode::StripPadding);
+echo "\n";
+
+// The full Hobbes quote from Wikipedia - check encoding length
+$hobbesQuote = 'Man is distinguished, not only by his reason, but by this singular passion from other animals, which is a lust of the mind, that by a perseverance of delight in the continued and indefatigable generation of knowledge, exceeds the short vehemence of any carnal pleasure.';
+$encoded = base85_encode($hobbesQuote, variant: Base85::Adobe, paddingMode: PaddingMode::StripPadding);
+echo strlen($hobbesQuote), "\n";
+echo strlen($encoded), "\n";
+
+// Verify "z" shorthand for four zero bytes
+$zeros = "\x00\x00\x00\x00";
+echo base85_encode($zeros, variant: Base85::Adobe, paddingMode: PaddingMode::StripPadding);
+echo "\n";
+
+// Decode "z" back to four zero bytes
+$decodedZeros = base85_decode("z", variant: Base85::Adobe);
+var_dump($decodedZeros === $zeros);
+
+// Verify encoding of "Hello world!" with delimiters
+echo base85_encode("Hello world!", variant: Base85::Adobe);
+echo "\n";
+
+// Decode "Hello world!" without delimiters
+echo base85_encode("Hello world!", variant: Base85::Adobe, paddingMode: PaddingMode::StripPadding);
+echo "\n";
+$decoded = base85_decode("87cURD]j7BEbo80", variant: Base85::Adobe);
+var_dump($decoded === "Hello world!");
+
+?>
+--EXPECT--
+9jqo^
+F*2M7
+269
+336
+z
+bool(true)
+<~87cURD]j7BEbo80~>
+87cURD]j7BEbo80
+bool(true)