patch 9.2.0420: channel: cannot handle binary data via channel callbacks
Commit:
https://github.com/vim/vim/commit/3ac7b974391c40ba61c7abec8c02ea3bacf818a4
Author: Yasuhiro Matsumoto <[email protected]>
Date: Wed Apr 29 19:48:05 2026 +0000
patch 9.2.0420: channel: cannot handle binary data via channel callbacks
Problem: channel: cannot handle binary data via channel callbacks
Solution: Add a blob channel mode that passes callback data as a Blob
(Yasuhiro Matsumoto).
closes: #20084
Signed-off-by: Yasuhiro Matsumoto <[email protected]>
Signed-off-by: Christian Brabandt <[email protected]>
diff --git a/runtime/doc/channel.txt b/runtime/doc/channel.txt
index 26cdc6a00..839dcc296 100644
--- a/runtime/doc/channel.txt
+++ b/runtime/doc/channel.txt
@@ -1,4 +1,4 @@
-*channel.txt* For Vim version 9.2. Last change: 2026 Apr 28
+*channel.txt* For Vim version 9.2. Last change: 2026 Apr 29
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -170,6 +170,7 @@ unreachable on the network.
"js" - Use JS (JavaScript) encoding, more efficient than JSON.
"nl" - Use messages that end in a NL character
"raw" - Use raw messages
+ "blob" - Use raw messages and pass callback data as a |Blob|
"lsp" - Use language server protocol encoding
"dap" - Use debug adapter protocol encoding
*channel-callback* *E921*
@@ -189,6 +190,8 @@ unreachable on the network.
excluding the NL.
When "mode" is "raw" the "msg" argument is the whole message
as a string.
+ When "mode" is "blob" the "msg" argument is the whole message
+ as a |Blob|.
For all callbacks: Use |function()| to bind it to arguments
and/or a Dictionary. Or use the form "dict.function" to bind
diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt
index 7dbc447a9..9516ecd12 100644
--- a/runtime/doc/version9.txt
+++ b/runtime/doc/version9.txt
@@ -52620,6 +52620,7 @@ Other ~
height. Also added the "scrollbar" sub-option to 'tabpanelopt'.
- Added the "noinsert" value to the 'wildmode' option for symmetry with the
'completeopt' option
+- Channel can handle |Blob| messages |channel-open-options|.
Platform specific ~
-----------------
diff --git a/src/channel.c b/src/channel.c
index a0b7b953e..e703a8208 100644
--- a/src/channel.c
+++ b/src/channel.c
@@ -3213,6 +3213,7 @@ channel_use_json_head(channel_T *channel, ch_part_T part)
may_invoke_callback(channel_T *channel, ch_part_T part)
{
char_u *msg = NULL;
+ blob_T *blob = NULL;
typval_T *listtv = NULL;
typval_T argv[CH_JSON_MAX_ARGS];
int seq_nr = -1;
@@ -3224,6 +3225,7 @@ may_invoke_callback(channel_T *channel, ch_part_T part)
buf_T *buffer = NULL;
char_u *p;
int called_otc; // one time callbackup
+ int raw_len = 0;
if (channel->ch_nb_close_cb != NULL)
// this channel is handled elsewhere (netbeans)
@@ -3384,15 +3386,42 @@ may_invoke_callback(channel_T *channel, ch_part_T part)
{
// For a raw channel we don't know where the message ends, just
// get everything we have.
- // Convert NUL to NL, the internal representation.
- msg = channel_get_all(channel, part, NULL);
+ raw_len = 0;
+ msg = channel_get_all(channel, part,
+ ch_mode == CH_MODE_BLOB ? &raw_len : NULL);
}
if (msg == NULL)
return FALSE; // out of memory (and avoids Coverity warning)
- argv[1].v_type = VAR_STRING;
- argv[1].vval.v_string = msg;
+ if (ch_mode == CH_MODE_BLOB)
+ {
+ blob = blob_alloc();
+ if (blob == NULL)
+ {
+ vim_free(msg);
+ return FALSE;
+ }
+ if (ga_grow(&blob->bv_ga, raw_len) == FAIL)
+ {
+ blob_free(blob);
+ vim_free(msg);
+ return FALSE;
+ }
+ if (raw_len > 0)
+ {
+ mch_memmove(blob->bv_ga.ga_data, msg, (size_t)raw_len);
+ blob->bv_ga.ga_len = raw_len;
+ }
+ argv[1].v_type = VAR_BLOB;
+ argv[1].vval.v_blob = blob;
+ ++blob->bv_refcount;
+ }
+ else
+ {
+ argv[1].v_type = VAR_STRING;
+ argv[1].vval.v_string = msg;
+ }
}
called_otc = FALSE;
@@ -3519,6 +3548,8 @@ may_invoke_callback(channel_T *channel, ch_part_T part)
if (listtv != NULL)
free_tv(listtv);
+ if (blob != NULL)
+ blob_unref(blob);
vim_free(msg);
return TRUE;
@@ -3654,6 +3685,9 @@ channel_part_info(channel_T *channel, dict_T *dict, char
*name, ch_part_T part)
case CH_MODE_RAW:
STR_LITERAL_SET(s, "RAW");
break;
+ case CH_MODE_BLOB:
+ STR_LITERAL_SET(s, "BLOB");
+ break;
case CH_MODE_JSON:
STR_LITERAL_SET(s, "JSON");
break;
@@ -4317,14 +4351,16 @@ channel_read_block(
readq_T *node;
ch_log(channel, "Blocking %s read, timeout: %d msec",
- mode == CH_MODE_RAW ? "RAW" : "NL", timeout);
+ mode == CH_MODE_RAW ? "RAW"
+ : mode == CH_MODE_BLOB ? "BLOB" : "NL",
timeout);
while (TRUE)
{
node = channel_peek(channel, part);
if (node != NULL)
{
- if (mode == CH_MODE_RAW || (mode == CH_MODE_NL
+ if (mode == CH_MODE_RAW || mode == CH_MODE_BLOB
+ || (mode == CH_MODE_NL
&& channel_first_nl(node) != NULL))
// got a complete message
break;
@@ -4348,7 +4384,7 @@ channel_read_block(
}
// We have a complete message now.
- if (mode == CH_MODE_RAW || outlen != NULL)
+ if (mode == CH_MODE_RAW || mode == CH_MODE_BLOB || outlen != NULL)
{
msg = channel_get_all(channel, part, outlen);
}
@@ -4384,7 +4420,8 @@ channel_read_block(
}
}
if (ch_log_active())
- ch_log(channel, "Returning %d bytes", (int)STRLEN(msg));
+ ch_log(channel, "Returning %d bytes",
+ outlen != NULL ? *outlen : (int)STRLEN(msg));
return msg;
}
@@ -4595,7 +4632,7 @@ common_channel_read(typval_T *argvars, typval_T *rettv,
int raw, int blob)
if (opt.jo_set & JO_TIMEOUT)
timeout = opt.jo_timeout;
- if (blob)
+ if (blob || mode == CH_MODE_BLOB)
{
int outlen = 0;
char_u *p = channel_read_block(channel, part,
@@ -5000,9 +5037,10 @@ ch_expr_common(typval_T *argvars, typval_T *rettv, int
eval)
part_send = channel_part_send(channel);
ch_mode = channel_get_mode(channel, part_send);
- if (ch_mode == CH_MODE_RAW || ch_mode == CH_MODE_NL)
+ if (ch_mode == CH_MODE_RAW || ch_mode == CH_MODE_BLOB
+ || ch_mode == CH_MODE_NL)
{
- emsg(_(e_cannot_use_evalexpr_sendexpr_with_raw_or_nl_channel));
+ emsg(_(e_cannot_use_evalexpr_sendexpr_with_raw_nl_or_blob_channel));
return;
}
diff --git a/src/errors.h b/src/errors.h
index c077118c6..384dfaab9 100644
--- a/src/errors.h
+++ b/src/errors.h
@@ -2377,8 +2377,8 @@ EXTERN char e_using_job_as_number[]
INIT(= N_("E910: Using a Job as a Number"));
EXTERN char e_using_job_as_float[]
INIT(= N_("E911: Using a Job as a Float"));
-EXTERN char e_cannot_use_evalexpr_sendexpr_with_raw_or_nl_channel[]
- INIT(= N_("E912: Cannot use ch_evalexpr()/ch_sendexpr() with a raw or
nl channel"));
+EXTERN char e_cannot_use_evalexpr_sendexpr_with_raw_nl_or_blob_channel[]
+ INIT(= N_("E912: Cannot use ch_evalexpr()/ch_sendexpr() with a raw, nl
or blob channel"));
EXTERN char e_using_channel_as_number[]
INIT(= N_("E913: Using a Channel as a Number"));
EXTERN char e_using_channel_as_float[]
diff --git a/src/job.c b/src/job.c
index 041a8e9b9..937d55f6b 100644
--- a/src/job.c
+++ b/src/job.c
@@ -27,6 +27,8 @@ handle_mode(typval_T *item, jobopt_T *opt, ch_mode_T *modep,
int jo)
*modep = CH_MODE_NL;
else if (STRCMP(val, "raw") == 0)
*modep = CH_MODE_RAW;
+ else if (STRCMP(val, "blob") == 0)
+ *modep = CH_MODE_BLOB;
else if (STRCMP(val, "js") == 0)
*modep = CH_MODE_JS;
else if (STRCMP(val, "json") == 0)
diff --git a/src/po/vim.pot b/src/po/vim.pot
index 8f43a89f7..4dd6ba909 100644
--- a/src/po/vim.pot
+++ b/src/po/vim.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Vim
"
"Report-Msgid-Bugs-To: [email protected]
"
-"POT-Creation-Date: 2026-04-27 18:39+0000
"
+"POT-Creation-Date: 2026-04-29 19:49+0000
"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE
"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>
"
"Language-Team: LANGUAGE <[email protected]>
"
@@ -6766,7 +6766,8 @@ msgstr ""
msgid "E911: Using a Job as a Float"
msgstr ""
-msgid "E912: Cannot use ch_evalexpr()/ch_sendexpr() with a raw or nl channel"
+msgid ""
+"E912: Cannot use ch_evalexpr()/ch_sendexpr() with a raw, nl or blob channel"
msgstr ""
msgid "E913: Using a Channel as a Number"
diff --git a/src/structs.h b/src/structs.h
index 14978739f..8429ebe29 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -2671,6 +2671,7 @@ typedef enum
{
CH_MODE_NL = 0,
CH_MODE_RAW,
+ CH_MODE_BLOB,
CH_MODE_JSON,
CH_MODE_JS,
CH_MODE_LSP, // Language Server Protocol (http + json)
diff --git a/src/testdir/test_channel.vim b/src/testdir/test_channel.vim
index abdaed0dc..f35a1b855 100644
--- a/src/testdir/test_channel.vim
+++ b/src/testdir/test_channel.vim
@@ -1390,6 +1390,64 @@ func Test_out_cb_lambda()
endtry
endfunc
+func Test_out_cb_blob_mode()
+ let g:Ch_blob_bytes = []
+ func OutBlobCb(chan, msg)
+ call assert_equal(v:t_blob, type(a:msg))
+ let g:Ch_blob_bytes += blob2list(a:msg)
+ endfunc
+
+ let cmd = [s:python, '-c',
+ \ 'import sys,time;'
+ \ .. 'sys.stdout.buffer.write(bytes([0, 1, 2, 10, 255]));'
+ \ .. 'sys.stdout.flush();'
+ \ .. 'time.sleep(0.1)']
+ let job = job_start(cmd, #{
+ \ out_mode: 'blob',
+ \ out_cb: 'OutBlobCb',
+ \ })
+ try
+ call WaitForAssert({-> assert_equal([0, 1, 2, 10, 255], g:Ch_blob_bytes)})
+ finally
+ call job_stop(job)
+ delfunc OutBlobCb
+ unlet g:Ch_blob_bytes
+ endtry
+endfunc
+
+func Test_pty_out_cb_blob_mode()
+ CheckUnix
+
+ let g:Ch_blob_bytes = []
+ func PtyBlobCb(chan, msg)
+ call assert_equal(v:t_blob, type(a:msg))
+ let g:Ch_blob_bytes += blob2list(a:msg)
+ endfunc
+
+ " Put the pty in raw mode so the line discipline does not translate LF
+ " to CRLF or strip NUL bytes, then write bytes that include NULs on
+ " both sides of an embedded LF.
+ let cmd = [s:python, '-c',
+ \ 'import os,sys,time;'
+ \ .. 'os.system("stty raw -echo");'
+ \ .. 'sys.stdout.buffer.write(bytes([65, 0, 66, 10, 67, 0, 68]));'
+ \ .. 'sys.stdout.flush();'
+ \ .. 'time.sleep(0.1)']
+ let job = job_start(cmd, #{
+ \ pty: 1,
+ \ out_mode: 'blob',
+ \ out_cb: 'PtyBlobCb',
+ \ })
+ try
+ call WaitForAssert({-> assert_equal(
+ \ [65, 0, 66, 10, 67, 0, 68], g:Ch_blob_bytes)})
+ finally
+ call job_stop(job)
+ delfunc PtyBlobCb
+ unlet g:Ch_blob_bytes
+ endtry
+endfunc
+
func Test_close_and_exit_cb()
let g:test_is_flaky = 1
let g:retdict = {'ret': {}}
diff --git a/src/version.c b/src/version.c
index 9873a0236..699db3c56 100644
--- a/src/version.c
+++ b/src/version.c
@@ -729,6 +729,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 420,
/**/
419,
/**/
--
--
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php
---
You received this message because you are subscribed to the Google Groups
"vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion visit
https://groups.google.com/d/msgid/vim_dev/E1wIB4m-007oCH-EJ%40256bit.org.