]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 9.2.0420: channel: cannot handle binary data via channel callbacks v9.2.0420
authorYasuhiro Matsumoto <mattn.jp@gmail.com>
Wed, 29 Apr 2026 19:48:05 +0000 (19:48 +0000)
committerChristian Brabandt <cb@256bit.org>
Wed, 29 Apr 2026 19:49:45 +0000 (19:49 +0000)
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 <mattn.jp@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
runtime/doc/channel.txt
runtime/doc/version9.txt
src/channel.c
src/errors.h
src/job.c
src/po/vim.pot
src/structs.h
src/testdir/test_channel.vim
src/version.c

index 26cdc6a0067dffdf6bcaa1c7278d97ff27395586..839dcc296e829ebccbc8f006df528861dbf0a7d8 100644 (file)
@@ -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
index 7dbc447a9cb235ad26c7344a0e04f3219734c924..9516ecd1250da1451a29aaa10a93830bd60690c3 100644 (file)
@@ -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 ~
 -----------------
index a0b7b953e2d68a371bd2de181da7364ba7a65504..e703a82089732480ab3dc7426c8a10cfd94cf610 100644 (file)
@@ -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;
     }
 
index c077118c6d9b2095e87fe98e18484463d9ba0c77..384dfaab9ca870786f0b9d20005badce4b6dc8b1 100644 (file)
@@ -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[]
index 041a8e9b93531a5b971b04ee2398e1f79cb48977..937d55f6be532caa7475838ba61d6461c1c006cf 100644 (file)
--- 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)
index 8f43a89f73ddba329dc8889f11883b95cca06f0f..4dd6ba9093d6586c1f71a77a12b1207375991940 100644 (file)
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: Vim\n"
 "Report-Msgid-Bugs-To: vim-dev@vim.org\n"
-"POT-Creation-Date: 2026-04-27 18:39+0000\n"
+"POT-Creation-Date: 2026-04-29 19:49+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -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"
index 14978739f59e35225ef9077f988408126a2c300f..8429ebe29e3468ccc7beccf7e08712d072be6e44 100644 (file)
@@ -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)
index abdaed0dc1773560626ec725668c5ea93e20d9b8..f35a1b8556bf8e6782b9e39801af28987a5635e5 100644 (file)
@@ -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': {}}
index 9873a0236f7e88b7837d4792e22f08bed4be0ce7..699db3c560cde8ea6bcd031b4a1596dd494cab5a 100644 (file)
@@ -729,6 +729,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    420,
 /**/
     419,
 /**/