patch 9.2.0961: base64_encode() gives wrong result for a zero byte
Commit:
https://github.com/vim/vim/commit/9d1f0081c74bd32ccde2bc66a4702eef7101d4fe
Author: Foxe Chen <
chen...@gmail.com>
Date: Mon Aug 17 19:22:08 2026 +0000
patch 9.2.0961: base64_encode() gives wrong result for a zero byte
Problem: base64_encode() encodes a zero byte in the last group as
padding.
Solution: Decide the padding from the number of remaining input bytes,
refactor the code and move to misc2.c (Foxe Chen).
related: #21018
Signed-off-by: Foxe Chen <
chen...@gmail.com>
Signed-off-by: Christian Brabandt <
c...@256bit.org>
diff --git a/src/evalfunc.c b/src/evalfunc.c
index 1b8cce9da..92a316354 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -3716,148 +3716,6 @@ f_balloon_split(typval_T *argvars, typval_T *rettv UNUSED)
# endif
#endif
-// Base64 character set
-static const char_u base64_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
-
-// Base64 decoding table (initialized in init_base64_dec_table() below)
-static char_u base64_dec_table[256];
-
-/*
- * Initialize the base64 decoding table
- */
- static void
-init_base64_dec_table(void)
-{
- static int base64_dec_tbl_initialized = FALSE;
-
- if (base64_dec_tbl_initialized)
- return;
-
- // Unsupported characters are set to 0xFF
- vim_memset(base64_dec_table, 0xFF, sizeof(base64_dec_table));
-
- // Initialize the index for the base64 alphabets
- for (size_t i = 0; i < sizeof(base64_table) - 1; i++)
- base64_dec_table[(char_u)base64_table[i]] = (char_u)i;
-
- // base64 padding character
- base64_dec_table['='] = 0;
-
- base64_dec_tbl_initialized = TRUE;
-}
-
-/*
- * Encode the bytes in "blob" using base-64 encoding.
- */
- static char_u *
-base64_encode(blob_T *blob)
-{
- size_t input_len = blob->bv_ga.ga_len;
- size_t encoded_len = ((input_len + 2) / 3) * 4;
- char_u *data = blob->bv_ga.ga_data;
-
- char_u *encoded = alloc(encoded_len + 1);
- if (encoded == NULL)
- return NULL;
-
- size_t i, j;
- for (i = 0, j = 0; i < input_len;)
- {
- int_u octet_a = i < input_len ? data[i++] : 0;
- int_u octet_b = i < input_len ? data[i++] : 0;
- int_u octet_c = i < input_len ? data[i++] : 0;
-
- int_u triple = (octet_a << 16) | (octet_b << 8) | octet_c;
-
- encoded[j++] = base64_table[(triple >> 18) & 0x3F];
- encoded[j++] = base64_table[(triple >> 12) & 0x3F];
- encoded[j++] = (!octet_b && i >= input_len) ? '='
- : base64_table[(triple >> 6) & 0x3F];
- encoded[j++] = (!octet_c && i >= input_len) ? '='
- : base64_table[triple & 0x3F];
- }
- encoded[j] = NUL;
-
- return encoded;
-}
-
-/*
- * Decode the string "data" using base-64 encoding.
- */
- static void
-base64_decode(const char_u *data, blob_T *blob)
-{
- size_t input_len = STRLEN(data);
-
- if (input_len == 0)
- return;
-
- if (input_len % 4 != 0)
- {
- // Invalid input length
- semsg(_(e_invalid_argument_str), data);
- return;
- }
-
- init_base64_dec_table();
-
- size_t decoded_len = (input_len / 4) * 3;
- if (data[input_len - 1] == '=')
- decoded_len--;
- if (data[input_len - 2] == '=')
- decoded_len--;
-
- size_t i, j;
- for (i = 0, j = 0; i < input_len;)
- {
- int_u sextet_a = base64_dec_table[(char_u)data[i++]];
- int_u sextet_b = base64_dec_table[(char_u)data[i++]];
- int_u sextet_c = base64_dec_table[(char_u)data[i++]];
- int_u sextet_d = base64_dec_table[(char_u)data[i++]];
-
- if (sextet_a == 0xFF || sextet_b == 0xFF || sextet_c == 0xFF
- || sextet_d == 0xFF)
- {
- // Invalid character
- semsg(_(e_invalid_argument_str), data);
- ga_clear(&blob->bv_ga);
- return;
- }
-
- int_u triple = (sextet_a << 18) | (sextet_b << 12)
- | (sextet_c << 6) | sextet_d;
-
- if (j < decoded_len)
- {
- ga_append(&blob->bv_ga, (triple >> 16) & 0xFF);
- j++;
- }
- if (j < decoded_len)
- {
- ga_append(&blob->bv_ga, (triple >> 8) & 0xFF);
- j++;
- }
- if (j < decoded_len)
- {
- ga_append(&blob->bv_ga, triple & 0xFF);
- j++;
- }
-
- if (j == decoded_len)
- {
- // Check for invalid padding bytes (based on the
- // "Base64 Malleability in Practice" ACM paper).
- if ((data[input_len - 2] == '=' && ((sextet_b & 0xF) != 0))
- || ((data[input_len - 1] == '=') && ((sextet_c & 0x3) != 0)))
- {
- semsg(_(e_invalid_argument_str), data);
- ga_clear(&blob->bv_ga);
- return;
- }
- }
- }
-}
-
/*
* "base64_decode(string)" function
*/
@@ -3872,7 +3730,7 @@ f_base64_decode(typval_T *argvars, typval_T *rettv)
char_u *str = tv_get_string_chk(&argvars[0]);
if (str != NULL)
- base64_decode(str, rettv->vval.v_blob);
+ base64_decode(str, STRLEN(str), &rettv->vval.v_blob->bv_ga);
}
/*
@@ -3889,7 +3747,8 @@ f_base64_encode(typval_T *argvars, typval_T *rettv)
blob_T *blob = argvars->vval.v_blob;
if (blob != NULL)
- rettv->vval.v_string = base64_encode(blob);
+ rettv->vval.v_string =
+ base64_encode(blob->bv_ga.ga_data, blob->bv_ga.ga_len);
}
/*
diff --git a/src/misc2.c b/src/misc2.c
index 25dab5ed1..a33591cc4 100644
--- a/src/misc2.c
+++ b/src/misc2.c
@@ -3316,3 +3316,152 @@ mergesort_list(
return head;
}
+
+static const char_u base64_table[] =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+static char_u base64_dec_table[256];
+
+#define BASE64_ENCODED_LEN(len) ((((len) + 2) / 3) * 4 + 1)
+
+/*
+ * Initialize the base64 decoding table
+ */
+ static void
+init_base64_dec_table(void)
+{
+ static int base64_dec_tbl_initialized = FALSE;
+
+ if (base64_dec_tbl_initialized)
+ return;
+
+ vim_memset(base64_dec_table, 0xFF, sizeof(base64_dec_table));
+ for (size_t i = 0; i < sizeof(base64_table) - 1; i++)
+ base64_dec_table[(char_u)base64_table[i]] = (char_u)i;
+ base64_dec_table['='] = 0;
+
+ base64_dec_tbl_initialized = TRUE;
+}
+
+/*
+ * Base64 encode "src[len]" into caller-supplied "dst", which must have room for
+ * at least BASE64_ENCODED_LEN(len) bytes (NUL terminated). Returns the number
+ * of bytes written, excluding the NUL.
+ */
+ long
+base64_encode_buf(char_u *dst, const char_u *src, size_t len)
+{
+ size_t i, j;
+
+ for (i = 0, j = 0; i < len;)
+ {
+ size_t remaining = len - i;
+
+ int_u octet_a = src[i++];
+ int_u octet_b = remaining > 1 ? src[i++] : 0;
+ int_u octet_c = remaining > 2 ? src[i++] : 0;
+
+ int_u triple = (octet_a << 16) | (octet_b << 8) | octet_c;
+
+ dst[j++] = base64_table[(triple >> 18) & 0x3F];
+ dst[j++] = base64_table[(triple >> 12) & 0x3F];
+ dst[j++] = remaining > 1
+ ? base64_table[(triple >> 6) & 0x3F]
+ : '=';
+ dst[j++] = remaining > 2
+ ? base64_table[triple & 0x3F]
+ : '=';
+ }
+
+ dst[j] = NUL;
+ return (long)j;
+}
+
+/*
+ * Base64-encode "data[len]". Returns an allocated NUL-terminated string, or
+ * NULL on OOM. Caller frees with vim_free().
+ */
+ char_u *
+base64_encode(const char_u *data, size_t len)
+{
+ char_u *encoded = alloc(BASE64_ENCODED_LEN(len));
+
+ if (encoded == NULL)
+ return NULL;
+ base64_encode_buf(encoded, data, len);
+ return encoded;
+}
+
+/*
+ * Decode base64 text "data[len]" (len must be a multiple of 4) into growarray
+ * "out" (appended byte-by-byte, e.g. a blob's bv_ga). Returns OK on success and
+ * FAIL on failure.
+ */
+ int
+base64_decode(const char_u *data, size_t len, garray_T *out)
+{
+ if (len == 0)
+ return OK;
+
+ if (len % 4 != 0)
+ {
+ semsg(_(e_invalid_argument_str), data);
+ return FAIL;
+ }
+
+ init_base64_dec_table();
+
+ size_t decoded_len = (len / 4) * 3;
+ if (data[len - 1] == '=')
+ decoded_len--;
+ if (data[len - 2] == '=')
+ decoded_len--;
+
+ size_t i, j;
+ for (i = 0, j = 0; i < len;)
+ {
+ int_u sextet_a = base64_dec_table[(char_u)data[i++]];
+ int_u sextet_b = base64_dec_table[(char_u)data[i++]];
+ int_u sextet_c = base64_dec_table[(char_u)data[i++]];
+ int_u sextet_d = base64_dec_table[(char_u)data[i++]];
+
+ if (sextet_a == 0xFF || sextet_b == 0xFF || sextet_c == 0xFF
+ || sextet_d == 0xFF)
+ {
+ semsg(_(e_invalid_argument_str), data);
+ ga_clear(out);
+ return FAIL;
+ }
+
+ int_u triple = (sextet_a << 18) | (sextet_b << 12)
+ | (sextet_c << 6) | sextet_d;
+
+ if (j < decoded_len)
+ {
+ ga_append(out, (triple >> 16) & 0xFF);
+ j++;
+ }
+ if (j < decoded_len)
+ {
+ ga_append(out, (triple >> 8) & 0xFF);
+ j++;
+ }
+ if (j < decoded_len)
+ {
+ ga_append(out, triple & 0xFF);
+ j++;
+ }
+
+ if (j == decoded_len)
+ {
+ if ((data[len - 2] == '=' && ((sextet_b & 0xF) != 0))
+ || ((data[len - 1] == '=') && ((sextet_c & 0x3) != 0)))
+ {
+ semsg(_(e_invalid_argument_str), data);
+ ga_clear(out);
+ return FAIL;
+ }
+ }
+ }
+ return OK;
+}
diff --git a/src/proto/
misc2.pro b/src/proto/
misc2.pro
index af779259a..95a5a8fd4 100644
--- a/src/proto/
misc2.pro
+++ b/src/proto/
misc2.pro
@@ -66,4 +66,7 @@ int cmp_keyvalue_value_n(const void *a, const void *b);
int cmp_keyvalue_value_i(const void *a, const void *b);
int cmp_keyvalue_value_ni(const void *a, const void *b);
void *mergesort_list(void *head, void *(*get_next)(void *), void (*set_next)(void *, void *), void *(*get_prev)(void *), void (*set_prev)(void *, void *), int (*compare)(const void *, const void *));
+long base64_encode_buf(char_u *dst, const char_u *src, size_t len);
+char_u *base64_encode(const char_u *data, size_t len);
+int base64_decode(const char_u *data, size_t len, garray_T *out);
/* vim: set ft=c : */
diff --git a/src/testdir/test_functions.vim b/src/testdir/test_functions.vim
index 7052ad171..e3693c913 100644
--- a/src/testdir/test_functions.vim
+++ b/src/testdir/test_functions.vim
@@ -4601,6 +4601,9 @@ func Test_base64_encoding()
call assert_equal(0z00, base64_decode("===="))
call assert_equal(0z, base64_decode(""))
+ #" a zero byte in the last group is not padding
+ call assert_equal('AQAC', base64_encode(0z010002))
+
#" Test for invalid padding
call assert_equal('Hello', g:Blob2Str(base64_decode("SGVsbG8=")))
call assert_fails('call base64_decode("SGVsbG9=")', 'E475:')
diff --git a/src/version.c b/src/version.c
index 19542bdde..2033abfa4 100644
--- a/src/version.c
+++ b/src/version.c
@@ -763,6 +763,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 961,
/**/
960,
/**/