patch 9.1.0980: no support for base64 en-/decoding functions in Vim Script
Problem: no support for base64 en-/decoding functions in Vim Script
(networkhermit)
Solution: Add the base64_encode() and base64_decode() functions
(Yegappan Lakshmanan)
fixes: #16291
closes: #16330
Signed-off-by: Yegappan Lakshmanan <yegappan@yahoo.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt
index ac10590..c82ff81 100644
--- a/runtime/doc/builtin.txt
+++ b/runtime/doc/builtin.txt
@@ -1,4 +1,4 @@
-*builtin.txt* For Vim version 9.1. Last change: 2024 Dec 03
+*builtin.txt* For Vim version 9.1. Last change: 2024 Dec 30
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -67,6 +67,8 @@
balloon_gettext() String current text in the balloon
balloon_show({expr}) none show {expr} inside the balloon
balloon_split({msg}) List split {msg} as used for a balloon
+base64_decode({string}) Blob base-64 decode {string} characters
+base64_encode({blob}) String base-64 encode the bytes in {blob}
bindtextdomain({package}, {path})
Bool bind text domain to specified path
blob2list({blob}) List convert {blob} into a list of numbers
@@ -1169,6 +1171,43 @@
Return type: list<dict<any>>
+base64_decode({string}) *base64_decode()*
+ Return a Blob containing the bytes decoded from the base64
+ characters in {string}.
+
+ The {string} argument should contain only base64-encoded
+ characters and should have a length that is a multiple of 4.
+
+ Returns an empty blob on error.
+
+ Examples: >
+ " Write the decoded contents to a binary file
+ call writefile(base64_decode(s), 'tools.bmp')
+ " Decode a base64-encoded string
+ echo list2str(blob2list(base64_decode(encodedstr)))
+<
+ Can also be used as a |method|: >
+ GetEncodedString()->base64_decode()
+<
+ Return type: |Blob|
+
+
+base64_encode({blob}) *base64_encode()*
+ Return a base64-encoded String representing the bytes in
+ {blob}. The base64 alphabet defined in RFC 4648 is used.
+
+ Examples: >
+ " Encode the contents of a binary file
+ echo base64_encode(readblob('somefile.bin'))
+ " Encode a string
+ echo base64_encode(list2blob(str2list(somestr)))
+<
+ Can also be used as a |method|: >
+ GetBinaryData()->base64_encode()
+<
+ Return type: |String|
+
+
balloon_gettext() *balloon_gettext()*
Return the current text in the balloon. Only for the string,
not used for the List. Returns an empty string if balloon
diff --git a/runtime/doc/tags b/runtime/doc/tags
index b2aa064..751a3f9 100644
--- a/runtime/doc/tags
+++ b/runtime/doc/tags
@@ -6164,6 +6164,8 @@
balloon_split() builtin.txt /*balloon_split()*
bar motion.txt /*bar*
bars help.txt /*bars*
+base64_decode() builtin.txt /*base64_decode()*
+base64_encode() builtin.txt /*base64_encode()*
base_font_name_list mbyte.txt /*base_font_name_list*
basic.vim syntax.txt /*basic.vim*
beep options.txt /*beep*
diff --git a/runtime/doc/todo.txt b/runtime/doc/todo.txt
index 342332e..48937b1 100644
--- a/runtime/doc/todo.txt
+++ b/runtime/doc/todo.txt
@@ -1,4 +1,4 @@
-*todo.txt* For Vim version 9.1. Last change: 2024 Dec 16
+*todo.txt* For Vim version 9.1. Last change: 2024 Dec 30
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -4198,8 +4198,6 @@
char2hex() convert char string to hex string.
crypt() encrypt string
decrypt() decrypt string
- base64enc() base 64 encoding
- base64dec() base 64 decoding
attributes() return file protection flags "drwxrwxrwx"
shorten(fname) shorten a file name, like home_replace()
perl(cmd) call Perl and return string
diff --git a/runtime/doc/usr_41.txt b/runtime/doc/usr_41.txt
index 36907d2..fdb9934 100644
--- a/runtime/doc/usr_41.txt
+++ b/runtime/doc/usr_41.txt
@@ -1,4 +1,4 @@
-*usr_41.txt* For Vim version 9.1. Last change: 2024 Nov 11
+*usr_41.txt* For Vim version 9.1. Last change: 2024 Dec 30
VIM USER MANUAL - by Bram Moolenaar
@@ -1263,6 +1263,8 @@
json_decode() decode a JSON string to Vim types
js_encode() encode an expression to a JSON string
js_decode() decode a JSON string to Vim types
+ base64_encode() encode a blob into a base64 string
+ base64_decode() decode a base64 string into a blob
err_teapot() give error 418 or 503
Jobs: *job-functions*
diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt
index bb39c43..af63702 100644
--- a/runtime/doc/version9.txt
+++ b/runtime/doc/version9.txt
@@ -1,4 +1,4 @@
-*version9.txt* For Vim version 9.1. Last change: 2024 Dec 29
+*version9.txt* For Vim version 9.1. Last change: 2024 Dec 30
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -41629,13 +41629,15 @@
Functions: ~
+|base64_decode()| decode a base64 string into a blob
+|base64_encode()| encode a blob into a base64 string
|bindtextdomain()| set message lookup translation base path
|diff()| diff two Lists of strings
|filecopy()| copy a file {from} to {to}
|foreach()| apply function to List items
+|getcellpixels()| get List of terminal cell pixel size
|getcmdcomplpat()| Shell command line completion
|getcmdprompt()| get prompt for input()/confirm()
-|getcellpixels()| get List of terminal cell pixel size
|getregion()| get a region of text from a buffer
|getregionpos()| get a list of positions for a region
|id()| get unique identifier for a Dict, List, Object,
diff --git a/src/evalfunc.c b/src/evalfunc.c
index b2905da..ec108b0c 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -28,6 +28,8 @@
static void f_balloon_split(typval_T *argvars, typval_T *rettv);
# endif
#endif
+static void f_base64_encode(typval_T *argvars, typval_T *rettv);
+static void f_base64_decode(typval_T *argvars, typval_T *rettv);
static void f_bindtextdomain(typval_T *argvars, typval_T *rettv);
static void f_byte2line(typval_T *argvars, typval_T *rettv);
static void f_call(typval_T *argvars, typval_T *rettv);
@@ -1834,6 +1836,10 @@
NULL
#endif
},
+ {"base64_decode", 1, 1, FEARG_1, arg1_string,
+ ret_blob, f_base64_decode},
+ {"base64_encode", 1, 1, FEARG_1, arg1_blob,
+ ret_string, f_base64_encode},
{"bindtextdomain", 2, 2, 0, arg2_string,
ret_bool, f_bindtextdomain},
{"blob2list", 1, 1, FEARG_1, arg1_blob,
@@ -3479,6 +3485,182 @@
# 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
+ */
+ static void
+f_base64_decode(typval_T *argvars, typval_T *rettv)
+{
+ if (check_for_string_arg(argvars, 0) == FAIL)
+ return;
+
+ if (rettv_blob_alloc(rettv) == FAIL)
+ return;
+
+ char_u *str = tv_get_string_chk(&argvars[0]);
+ if (str != NULL)
+ base64_decode(str, rettv->vval.v_blob);
+}
+
+/*
+ * "base64_encode(blob)" function
+ */
+ static void
+f_base64_encode(typval_T *argvars, typval_T *rettv)
+{
+ if (check_for_blob_arg(argvars, 0) == FAIL)
+ return;
+
+ rettv->v_type = VAR_STRING;
+ rettv->vval.v_string = NULL;
+
+ blob_T *blob = argvars->vval.v_blob;
+ if (blob != NULL)
+ rettv->vval.v_string = base64_encode(blob);
+}
+
/*
* Get the buffer from "arg" and give an error and return NULL if it is not
* valid.
diff --git a/src/testdir/test_functions.vim b/src/testdir/test_functions.vim
index 8b2518f..f80754f 100644
--- a/src/testdir/test_functions.vim
+++ b/src/testdir/test_functions.vim
@@ -4206,4 +4206,55 @@
endif
endfunc
+func Str2Blob(s)
+ return list2blob(str2list(a:s))
+endfunc
+
+func Blob2Str(b)
+ return list2str(blob2list(a:b))
+endfunc
+
+" Test for the base64_encode() and base64_decode() functions
+func Test_base64_encoding()
+ let lines =<< trim END
+ #" Test for encoding/decoding the RFC-4648 alphabets
+ VAR s = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
+ for i in range(64)
+ call assert_equal($'{s[i]}A==', base64_encode(list2blob([i << 2])))
+ call assert_equal(list2blob([i << 2]), base64_decode($'{s[i]}A=='))
+ endfor
+
+ #" Test for encoding with padding
+ call assert_equal('TQ==', base64_encode(g:Str2Blob("M")))
+ call assert_equal('TWE=', base64_encode(g:Str2Blob("Ma")))
+ call assert_equal('TWFu', g:Str2Blob("Man")->base64_encode())
+ call assert_equal('', base64_encode(0z))
+ call assert_equal('', base64_encode(g:Str2Blob("")))
+
+ #" Test for decoding with padding
+ call assert_equal('light work.', g:Blob2Str(base64_decode("bGlnaHQgd29yay4=")))
+ call assert_equal('light work', g:Blob2Str(base64_decode("bGlnaHQgd29yaw==")))
+ call assert_equal('light wor', g:Blob2Str("bGlnaHQgd29y"->base64_decode()))
+ call assert_equal(0z00, base64_decode("===="))
+ call assert_equal(0z, base64_decode(""))
+
+ #" Test for invalid padding
+ call assert_equal('Hello', g:Blob2Str(base64_decode("SGVsbG8=")))
+ call assert_fails('call base64_decode("SGVsbG9=")', 'E475:')
+ call assert_fails('call base64_decode("SGVsbG9")', 'E475:')
+ call assert_equal('Hell', g:Blob2Str(base64_decode("SGVsbA==")))
+ call assert_fails('call base64_decode("SGVsbA=")', 'E475:')
+ call assert_fails('call base64_decode("SGVsbA")', 'E475:')
+ call assert_fails('call base64_decode("SGVsbA====")', 'E475:')
+
+ #" Error case
+ call assert_fails('call base64_decode("b")', 'E475: Invalid argument: b')
+ call assert_fails('call base64_decode("<<==")', 'E475: Invalid argument: <<==')
+
+ call assert_fails('call base64_encode([])', 'E1238: Blob required for argument 1')
+ call assert_fails('call base64_decode([])', 'E1174: String required for argument 1')
+ END
+ call v9.CheckLegacyAndVim9Success(lines)
+endfunc
+
" vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/version.c b/src/version.c
index e63645d..5f1649e 100644
--- a/src/version.c
+++ b/src/version.c
@@ -705,6 +705,8 @@
static int included_patches[] =
{ /* Add new patch number below this line */
/**/
+ 980,
+/**/
979,
/**/
978,