diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 52ac1f9e2..b006113ec 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -54,7 +54,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v4.35.2 + uses: github/codeql-action/init@v4.35.3 with: languages: ${{ matrix.language }} trap-caching: false @@ -64,7 +64,7 @@ jobs: run: python3 .github/workflows/ci.py build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4.35.2 + uses: github/codeql-action/analyze@v4.35.3 - name: Run govulncheck if: matrix.language == 'go' diff --git a/docs/dnd-protocol.rst b/docs/dnd-protocol.rst index 8e2ba929d..b3cd0846e 100644 --- a/docs/dnd-protocol.rst +++ b/docs/dnd-protocol.rst @@ -391,7 +391,7 @@ examine the :ref:`machine_id` sent with the enable drag offers from the URI list. To request data for a particular entry, terminals send an escape code of the form:: - OSC _dnd_code ; t=k:x=idx ; base64 encoded file data ST + OSC _dnd_code ; t=k:x=idx ST Here ``idx`` is the one based index into the list of entries in the ``text/uri-list`` MIME type. Then the client can respond with the data @@ -419,11 +419,6 @@ adding ``Y=parent-handle:y=num`` to the escape codes above. Here is the one based index into the list of entries in the directory. Thus, the set of keys ``x, y, Y`` uniquely determine an entry. -Once all data for a dirctory is transmitted, the client informs the terminal emulator of -completion with:: - - OSC _dnd_code ; t=k:Y=handle ; ST - If any error occurs in the client while reading the data, it can inform the terminal using:: diff --git a/go.mod b/go.mod index 37eb6028a..056dc2f12 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.26.3 require ( github.com/ALTree/bigfloat v0.2.0 - github.com/alecthomas/chroma/v2 v2.23.1 + github.com/alecthomas/chroma/v2 v2.24.1 github.com/bmatcuk/doublestar/v4 v4.10.0 github.com/dlclark/regexp2 v1.12.0 github.com/ebitengine/purego v0.10.0 @@ -14,7 +14,7 @@ require ( github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b - github.com/klauspost/compress v1.18.5 + github.com/klauspost/compress v1.18.6 github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1 github.com/kovidgoyal/go-parallel v1.1.1 github.com/kovidgoyal/go-shm v1.0.0 @@ -22,12 +22,12 @@ require ( github.com/nwaples/rardecode/v2 v2.2.2 github.com/seancfoley/ipaddress-go v1.7.1 github.com/sgtdi/fswatcher v1.2.0 - github.com/shirou/gopsutil/v4 v4.26.3 + github.com/shirou/gopsutil/v4 v4.26.4 github.com/ulikunitz/xz v0.5.15 github.com/zeebo/xxh3 v1.1.0 golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b golang.org/x/image v0.39.0 - golang.org/x/sys v0.43.0 + golang.org/x/sys v0.44.0 golang.org/x/text v0.36.0 howett.net/plist v1.0.1 ) diff --git a/go.sum b/go.sum index c4c9a3a48..0e6767435 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/ALTree/bigfloat v0.2.0 h1:AwNzawrpFuw55/YDVlcPw0F0cmmXrmngBHhVrvdXPvM github.com/ALTree/bigfloat v0.2.0/go.mod h1:+NaH2gLeY6RPBPPQf4aRotPPStg+eXc8f9ZaE4vRfD4= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY= -github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= +github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM= +github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= @@ -28,8 +28,8 @@ github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1p github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= -github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1 h1:rMY/hWfcVzBm6BLX6YLA+gLJEpuXBed/VP6YEkXt8R4= @@ -56,8 +56,8 @@ github.com/seancfoley/ipaddress-go v1.7.1 h1:fDWryS+L8iaaH5RxIKbY0xB5Z+Zxk8xoXLN github.com/seancfoley/ipaddress-go v1.7.1/go.mod h1:TQRZgv+9jdvzHmKoPGBMxyiaVmoI0rYpfEk8Q/sL/Iw= github.com/sgtdi/fswatcher v1.2.0 h1:uSJuMc3/Eo/vaPnZWpJ42EFYb5j38cZENmkszOV0yhw= github.com/sgtdi/fswatcher v1.2.0/go.mod h1:smzXnaqu0SYJQNIwGLLkvRkpH4RdEACB7avMSsSaqjQ= -github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= -github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/shirou/gopsutil/v4 v4.26.4 h1:B4SXVbcwTyrocPHEmWBC4uCYr4Xcu3MK1TXqbprAOWY= +github.com/shirou/gopsutil/v4 v4.26.4/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= @@ -78,8 +78,8 @@ golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww= golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/kittens/dnd/drag.go b/kittens/dnd/drag.go index b14d69439..2772e6ade 100644 --- a/kittens/dnd/drag.go +++ b/kittens/dnd/drag.go @@ -22,11 +22,10 @@ import ( var _ = fmt.Print type data_request struct { - drag_source *drag_source - send_remote_data bool - index int - write_id loop.IdType - base64 streaming_base64.StreamingBase64Encoder + drag_source *drag_source + index int + write_id loop.IdType + base64 streaming_base64.StreamingBase64Encoder } type remote_data_item struct { @@ -51,7 +50,7 @@ type drag_status struct { current_remote_file *remote_data_item dir_handle_counter int remote_item_write_id loop.IdType - remote_data_was_sent bool + remote_data_requests []int } func find_drag_image(drag_sources map[string]*drag_source) image.Image { @@ -66,12 +65,10 @@ func find_drag_image(drag_sources map[string]*drag_source) image.Image { } } var uri_list []string - if ds := drag_sources["text/uri-list"]; ds != nil && len(ds.data) > 0 { - if q, err := parse_uri_list(string(ds.data)); err == nil { - for _, path := range q { - if path != "" { - uri_list = append(uri_list, path) - } + if ds := drag_sources["text/uri-list"]; ds != nil { + for _, e := range ds.uri_list { + if e.path != "" { + uri_list = append(uri_list, e.path) } } } @@ -102,12 +99,10 @@ func (dnd *dnd) set_drag_image_text() (err error) { } } if icon == "" { - if ds := dnd.drag_sources["text/uri-list"]; ds != nil && len(ds.data) > 0 { - if q, err := parse_uri_list(string(ds.data)); err == nil { - for _, path := range q { - if path != "" && from_path(path) { - break - } + if ds := dnd.drag_sources["text/uri-list"]; ds != nil { + for _, e := range ds.uri_list { + if e.path != "" && from_path(e.path) { + break } } } @@ -219,7 +214,7 @@ func (dnd *dnd) reset_drag() { dnd.drag_status = drag_status{} } -func (dnd *dnd) on_drag_event(x, y, operation, Y int) (err error) { +func (dnd *dnd) on_drag_event(x, y, operation int) (err error) { switch x { case 1: dnd.drag_status.accepted_mime = y @@ -230,21 +225,22 @@ func (dnd *dnd) on_drag_event(x, y, operation, Y int) (err error) { case 4: was_dropped := dnd.drag_status.dropped was_move := dnd.drag_status.accepted_operation == 2 - was_remote := dnd.drag_status.remote_data_was_sent dnd.reset_drag() if was_dropped && dnd.has_exit_on("drag-finish") { dnd.lp.Quit(0) } - if was_dropped && was_move && was_remote { + if was_dropped && was_move { if ds := dnd.drag_sources["text/uri-list"]; ds != nil { for _, item := range ds.uri_list { - if item.metadata.IsDir() { - err = os.RemoveAll(item.path) - } else { - err = os.Remove(item.path) - } - if err != nil { - return err + if item.was_sent { + if item.metadata.IsDir() { + err = os.RemoveAll(item.path) + } else { + err = os.Remove(item.path) + } + if err != nil { + return err + } } } } @@ -256,7 +252,7 @@ func (dnd *dnd) on_drag_event(x, y, operation, Y int) (err error) { } } case 5: - if err = dnd.handle_data_request(y, Y == 1); err != nil { + if err = dnd.handle_data_request(y); err != nil { return err } } @@ -272,28 +268,24 @@ func (dnd *dnd) finish_drag(errname string) { dnd.reset_drag() } -func (dnd *dnd) handle_data_request(idx int, send_remote_data bool) (err error) { +func (dnd *dnd) handle_data_request(idx int) (err error) { if idx < 0 || idx >= len(dnd.drag_status.offered_mimes) { dnd.finish_drag("EINVAL") return fmt.Errorf("terminal asked for drag data from MIME list with out of bounds index: %d", idx) } mime := dnd.drag_status.offered_mimes[idx] ds := dnd.drag_sources[mime] - send_remote_data = send_remote_data && mime == "text/uri-list" && len(ds.uri_list) > 0 for _, dr := range dnd.drag_status.data_requests { if dr.index == idx { dnd.finish_drag("EINVAL") return fmt.Errorf("terminal sent a duplicate drag data request") } } - dr := &data_request{drag_source: ds, send_remote_data: send_remote_data, index: idx} + dr := &data_request{drag_source: ds, index: idx} if ds.path == "" { dnd.lp.QueueDnDData(DC{Type: 'e', Y: idx, Payload: utils.UnsafeStringToBytes(base64.RawStdEncoding.EncodeToString(ds.data))}) dnd.lp.QueueDnDData(DC{Type: 'e', Y: idx}) // EOF - if !dr.send_remote_data { - return - } - return dnd.start_remote_data_send(ds) + return } else { if ds.file != nil { ds.file.Close() @@ -356,9 +348,7 @@ func (dnd *dnd) on_data_request_finished(i int) (err error) { dr.drag_source.file = nil } dnd.drag_status.data_requests = slices.Delete(dnd.drag_status.data_requests, i, i+1) - if dr.send_remote_data { - err = dnd.start_remote_data_send(dr.drag_source) - } else if len(dnd.drag_status.data_requests) > 0 { + if len(dnd.drag_status.data_requests) > 0 { err = dnd.send_data_for_data_request(0) } return @@ -386,6 +376,9 @@ func (dnd *dnd) send_remote_dir(path string, idx_in_uri_list, parent_dir_handle, dnd.finish_drag("EIO") return nil, err } + for dnd.drag_status.dir_handle_counter < 2 { + dnd.drag_status.dir_handle_counter++ + } handle := dnd.drag_status.dir_handle_counter dnd.drag_status.dir_handle_counter++ names := make([]string, 0, len(entries)) @@ -447,7 +440,11 @@ func (dnd *dnd) send_next_file_chunk() (err error) { func (dnd *dnd) next_remote_item() (err error) { if len(dnd.drag_status.remote_items) < 1 { - dnd.lp.QueueDnDData(DC{Type: 'k'}) // inform terminal remote data is finished + // current remote data request finished + dnd.drag_status.remote_data_requests = dnd.drag_status.remote_data_requests[1:] + if len(dnd.drag_status.remote_data_requests) > 0 { + return dnd.send_next_remote_data_request() + } if len(dnd.drag_status.data_requests) > 0 { err = dnd.send_data_for_data_request(0) } @@ -478,26 +475,44 @@ func (dnd *dnd) next_remote_item() (err error) { return } -func (dnd *dnd) start_remote_data_send(ds *drag_source) (err error) { - dnd.drag_status.dir_handle_counter = 2 - dnd.drag_status.remote_item_write_id = 0 - dnd.drag_status.remote_data_was_sent = true +func (dnd *dnd) on_drag_remote_data_request(idx int) (err error) { + ds := dnd.drag_sources["text/uri-list"] + if ds == nil || len(ds.uri_list) < 1 { + dnd.finish_drag("EINVAL") + return fmt.Errorf("terminal asked for drag data from URI list but no list present") + } + if idx < 0 || idx >= len(ds.uri_list) { + dnd.finish_drag("EINVAL") + return fmt.Errorf("terminal asked for drag data from URI list with out of bounds index: %d", idx) + } + ds.uri_list[idx].was_sent = true + dnd.drag_status.remote_data_requests = append(dnd.drag_status.remote_data_requests, idx) + if len(dnd.drag_status.remote_data_requests) == 1 { + err = dnd.send_next_remote_data_request() + } + return +} + +func (dnd *dnd) send_next_remote_data_request() (err error) { + if len(dnd.drag_status.remote_data_requests) == 0 { + return nil + } + i := dnd.drag_status.remote_data_requests[0] + x := dnd.drag_sources["text/uri-list"].uri_list[i] items := []*remote_data_item{} - for i, x := range ds.uri_list { - if x.metadata.IsDir() { - if children, err := dnd.send_remote_dir(x.path, i, 0, i); err != nil { - return err - } else { - items = append(items, children...) - } - } else if x.metadata.Mode().Type()&os.ModeSymlink != 0 { - if err = dnd.send_remote_symlink(x.path, i, 0, i); err != nil { - return err - } + if x.metadata.IsDir() { + if children, err := dnd.send_remote_dir(x.path, i, 0, i); err != nil { + return err } else { - f := remote_data_item{idx_in_parent: i, idx_in_uri_list: i, metadata: x.metadata, path: x.path} - dnd.drag_status.remote_items = append(dnd.drag_status.remote_items, &f) + items = append(items, children...) } + } else if x.metadata.Mode().Type()&os.ModeSymlink != 0 { + if err = dnd.send_remote_symlink(x.path, i, 0, i); err != nil { + return err + } + } else { + f := remote_data_item{idx_in_parent: i, idx_in_uri_list: i, metadata: x.metadata, path: x.path} + dnd.drag_status.remote_items = append(dnd.drag_status.remote_items, &f) } dnd.drag_status.remote_items = append(dnd.drag_status.remote_items, items...) if dnd.drag_status.remote_item_write_id == 0 { diff --git a/kittens/dnd/main.go b/kittens/dnd/main.go index 067883a01..4707da2cd 100644 --- a/kittens/dnd/main.go +++ b/kittens/dnd/main.go @@ -31,6 +31,7 @@ type uri_list_item struct { path, uri, human_name string file *os.File metadata os.FileInfo + was_sent bool } type drag_source struct { @@ -105,7 +106,7 @@ func (dnd *dnd) send_test_response(payload string) { } func (dnd *dnd) has_exit_on(event string) bool { - for _, e := range strings.Split(dnd.opts.ExitOn, ",") { + for e := range strings.SplitSeq(dnd.opts.ExitOn, ",") { if strings.TrimSpace(e) == event { return true } @@ -276,7 +277,9 @@ func (dnd *dnd) run_loop() (err error) { case 'E': return dnd.on_drag_error(cmd) case 'e': - return dnd.on_drag_event(cmd.X, cmd.Y, cmd.Operation, cmd.Yp) + return dnd.on_drag_event(cmd.X, cmd.Y, cmd.Operation) + case 'k': + return dnd.on_drag_remote_data_request(cmd.X - 1) } return nil } diff --git a/kitty/dnd.c b/kitty/dnd.c index d98e5be38..43332acf9 100644 --- a/kitty/dnd.c +++ b/kitty/dnd.c @@ -1452,6 +1452,56 @@ expand_png_data(Window *w, size_t idx) { static size_t last_total_image_size = 0; +static char** +parse_uri_list(Window *w, char *data, const ssize_t sz, size_t *num_uris_out) { + *num_uris_out = 0; + // First pass: count non-comment, non-empty lines + size_t count = 0; + char *p = data; + while (p - data < sz) { + char *eol = p + strcspn(p, "\r\n"); + char saved = *eol; *eol = '\0'; + char *end = eol; + while (end > p && (end[-1] == ' ' || end[-1] == '\t')) end--; + char saved_end = *end; *end = '\0'; + if (*p && *p != '#') count++; + *end = saved_end; + *eol = saved; + if (saved == '\0') break; + p = eol + 1; + while (*p == '\r' || *p == '\n') p++; + } + + char **result = calloc((count + 1), sizeof(const char*)); + if (!result) { cancel_drag(w, ENOMEM, "out of memory parsing uri list"); return NULL; } + + // Second pass: fill in decoded URI strings + size_t idx = 0; + p = data; + while (p - data < sz && idx < count) { + char *eol = p + strcspn(p, "\r\n"); + char saved = *eol; *eol = '\0'; + char *end = eol; + while (end > p && (end[-1] == ' ' || end[-1] == '\t')) end--; + *end = '\0'; + if (*p && *p != '#') { + char *decoded = strdup(p); + if (!decoded) { + for (size_t k = 0; k < idx; k++) free((char*)result[k]); + free(result); cancel_drag(w, ENOMEM, "out of memory parsing uri list"); return NULL; + } + result[idx++] = decoded; + } + *eol = saved; + if (saved == '\0') break; + p = eol + 1; + while (*p == '\r' || *p == '\n') p++; + } + *num_uris_out = idx; + return result; +} + + void drag_start(Window *w) { if (ds.state != DRAG_SOURCE_BEING_BUILT) abrt(EINVAL, "cannot start drag as drag source is not being built"); @@ -1530,6 +1580,13 @@ drag_start(Window *w) { // Free images and optional_data but keep the items array for later // data requests from the drop target for (size_t i = 0; i < ds.num_mimes; i++) { + if (ds.is_remote_client && ds.items[i].is_uri_list) { + if (ds.items[i].optional_data && ds.items[i].data_size) { + ds.items[i].uri_list = parse_uri_list( + w, (char*)ds.items[i].optional_data, ds.items[i].data_size, &ds.items[i].num_uris); + if (!ds.items[i].uri_list) return; + } else abrt(EINVAL, "remote client must pre-send text/uri-list data"); + } free(ds.items[i].optional_data); ds.items[i].optional_data = NULL; ds.items[i].data_size = 0; @@ -1581,6 +1638,30 @@ drag_free_data(Window *w, const char *mime_type, const char* data, size_t sz) { return 0; } +static bool +is_file_url(const char *url) { + return url != NULL && strlen(url) > sizeof("file://")-1 && memcmp(url, "file://", sizeof("file://")-1) == 0; +} + +static bool +request_remote_files(Window *w, size_t i) { +#define mi ds.items[i] + char buf[128]; + mi.remote_items = calloc(mi.num_uris, sizeof(mi.remote_items[0])); + if (!mi.remote_items) return false; + mi.num_remote_items = mi.num_uris; + for (size_t k = 0; k < mi.num_remote_items; k++) { + if (is_file_url(mi.uri_list[k])) { + int header_sz = snprintf(buf, sizeof(buf), "\x1b]%d;t=k:x=%zu", DND_CODE, k + 1); + queue_payload_to_child( + w->id, w->drag_source.client_id, &w->drag_source.pending, buf, header_sz, NULL, 0, false); + mi.remote_items[k].waiting_for_completion = true; + } + } + return true; +#undef mi +} + const char* drag_get_data(Window *w, const char *mime_type, size_t *sz, int *err_code) { *err_code = ENOENT; *sz = 0; @@ -1636,12 +1717,17 @@ drag_get_data(Window *w, const char *mime_type, size_t *sz, int *err_code) { } // No fd yet, request data from the client if (!ds.items[i].data_requested_from_client) { - char buf[128]; - ds.items[i].requested_remote_files = ds.is_remote_client && ds.items[i].is_uri_list; - int header_sz = snprintf(buf, sizeof(buf), "\x1b]%d;t=e:x=%d:y=%zu:Y=%d", - DND_CODE, DRAG_NOTIFY_FINISHED + 2, i, ds.items[i].requested_remote_files); - queue_payload_to_child(w->id, w->drag_source.client_id, &w->drag_source.pending, buf, header_sz, NULL, 0, false); ds.items[i].data_requested_from_client = true; + ds.items[i].requested_remote_files = ds.is_remote_client && ds.items[i].is_uri_list; + if (ds.items[i].requested_remote_files) { + if (!request_remote_files(w, i)) { *err_code = ENOMEM; return NULL; } + } else { + char buf[128]; + int header_sz = snprintf(buf, sizeof(buf), + "\x1b]%d;t=e:x=%d:y=%zu", DND_CODE, DRAG_NOTIFY_FINISHED + 2, i); + queue_payload_to_child( + w->id, w->drag_source.client_id, &w->drag_source.pending, buf, header_sz, NULL, 0, false); + } } *err_code = EAGAIN; return NULL; @@ -1752,73 +1838,6 @@ drag_process_item_data(Window *w, size_t idx, int has_more, const uint8_t *paylo } } -static char** -parse_uri_list(Window *w, int fd, size_t *num_uris_out) { - *num_uris_out = 0; - // Determine file size and read all data - off_t file_size = lseek(fd, 0, SEEK_END); - if (file_size < 0) { cancel_drag(w, EIO, "failed to read cached uri-list data"); return NULL; } - if (lseek(fd, 0, SEEK_SET) < 0) { cancel_drag(w, EIO, "failed to read cached uri-list data"); return NULL; } - RAII_ALLOC(char, buf, malloc((size_t)file_size + 1)); - if (!buf) { cancel_drag(w, ENOMEM, "out of memory processing uri list data"); return NULL; } - size_t total = 0; - while (total < (size_t)file_size) { - ssize_t n = read(fd, buf + total, (size_t)file_size - total); - if (n < 0) { - if (errno == EINTR) continue; - cancel_drag(w, EIO, "failed to read cached uri-list data"); return NULL; - } - if (n == 0) break; - total += (size_t)n; - } - buf[total] = '\0'; - - // First pass: count non-comment, non-empty lines - size_t count = 0; - char *p = buf; - while (*p) { - char *eol = p + strcspn(p, "\r\n"); - char saved = *eol; *eol = '\0'; - char *end = eol; - while (end > p && (end[-1] == ' ' || end[-1] == '\t')) end--; - char saved_end = *end; *end = '\0'; - if (*p && *p != '#') count++; - *end = saved_end; - *eol = saved; - if (saved == '\0') break; - p = eol + 1; - while (*p == '\r' || *p == '\n') p++; - } - - char **result = calloc((count + 1), sizeof(const char*)); - if (!result) { cancel_drag(w, ENOMEM, "out of memory parsing uri list"); return NULL; } - - // Second pass: fill in decoded URI strings - size_t idx = 0; - p = buf; - while (*p && idx < count) { - char *eol = p + strcspn(p, "\r\n"); - char saved = *eol; *eol = '\0'; - char *end = eol; - while (end > p && (end[-1] == ' ' || end[-1] == '\t')) end--; - *end = '\0'; - if (*p && *p != '#') { - char *decoded = strdup(p); - if (!decoded) { - for (size_t k = 0; k < idx; k++) free((char*)result[k]); - free(result); cancel_drag(w, ENOMEM, "out of memory parsing uri list"); return NULL; - } - result[idx++] = decoded; - } - *eol = saved; - if (saved == '\0') break; - p = eol + 1; - while (*p == '\r' || *p == '\n') p++; - } - *num_uris_out = idx; - return result; -} - static int write_all(int fd, const void *buf, size_t sz) { size_t pos = 0; const char *p = buf; @@ -1832,6 +1851,11 @@ write_all(int fd, const void *buf, size_t sz) { static void finish_remote_data(Window *w, size_t item_idx) { + if (!ds.items[item_idx].fd_plus_one) { + int fd = open_item_tmpfile(); + if (fd < 0) abrt(EIO, "failed to open temp file to store modified uri list"); + ds.items[item_idx].fd_plus_one = fd + 1; + } const int fd = ds.items[item_idx].fd_plus_one - 1; ds.items[item_idx].requested_remote_files = false; if (safe_ftruncate(fd, 0) != 0) abrt(errno, "error updating uri list after all remote data received"); @@ -1919,10 +1943,12 @@ populate_dir_entries(Window *w, DragRemoteItem *ri) { while (ptr < end) { const char *p = memchr(ptr, 0, (size_t)(end - ptr)); size_t len = p ? (size_t)(p - ptr) : (size_t)(end - ptr); + DragRemoteItem *child = ri->children + ri->children_sz++; + child->parent = ri; if (len > 0) { char *name = strndup(ptr, len); if (!name) abrt(ENOMEM, "out of memory processing drag source item directory entries"); - ri->children[ri->children_sz++].dir_entry_name = name; + child->dir_entry_name = name; } ptr = p ? p + 1 : end; } @@ -1996,11 +2022,6 @@ toplevel_data_for_drag( Window *w, unsigned mime_item_idx, unsigned uri_item_idx, unsigned item_type, bool has_more, const uint8_t *payload, size_t payload_sz ) { - if (!mi.remote_items) { - mi.remote_items = calloc(mi.num_uris, sizeof(mi.remote_items[0])); - if (!mi.remote_items) abrt(ENOMEM, "out of memory processing drag source item"); - mi.num_remote_items = mi.num_uris; - } if (!mi.base_dir_for_remote_items) { int fd; mi.base_dir_for_remote_items = mktempdir_in_cache("dnd-drag-", &fd); @@ -2050,10 +2071,10 @@ find_by_handle(DragRemoteItem *parent, int handle, char *path_to_parent, size_t static void subdir_data_for_drag( Window *w, unsigned mime_item_idx, unsigned uri_item_idx, int handle, unsigned entry_num, unsigned item_type, - bool has_more, const uint8_t *payload, size_t payload_sz + bool has_more, const uint8_t *payload, size_t payload_sz, DragRemoteItem **ri ) { if (!mi.remote_items || uri_item_idx >= mi.num_remote_items) abrt(EINVAL, "drag source sub directory item uri list index out of range"); - DragRemoteItem *parent = NULL; + DragRemoteItem *parent = NULL; *ri = NULL; if (mi.currently_open_subdir) { if (mi.currently_open_subdir->type == handle) parent = mi.currently_open_subdir; else { @@ -2080,15 +2101,14 @@ subdir_data_for_drag( } } if (entry_num >= parent->children_sz) abrt(EINVAL, "drag source sub diretory index out of bounds"); - DragRemoteItem *ri = parent->children + entry_num; - if (!ri->started) { - ri->started = true; - ri->type = item_type; - base64_init_stream_decoder(&ri->base64_state); + *ri = parent->children + entry_num; + if (!(*ri)->started) { + (*ri)->started = true; + (*ri)->type = item_type; + base64_init_stream_decoder(&(*ri)->base64_state); } - add_payload(w, ri, has_more, payload, payload_sz, parent->fd_plus_one - 1); + add_payload(w, *ri, has_more, payload, payload_sz, parent->fd_plus_one - 1); } -#undef mi void drag_offer_start_to_child(Window *w, int32_t cell_x, int32_t cell_y, int32_t pixel_x, int32_t pixel_y) { @@ -2099,29 +2119,69 @@ drag_offer_start_to_child(Window *w, int32_t cell_x, int32_t cell_y, int32_t pix w->id, w->drag_source.client_id, &w->drag_source.pending, buf, header_size, NULL, 0, false); } +static void +finish_remote_data_if_all_items_received(Window *w, unsigned mime_item_idx) { + for (size_t i = 0; i < mi.num_remote_items; i++) { + if (mi.remote_items[i].waiting_for_completion && !mi.remote_items[i].completed) return; + } + finish_remote_data(w, mime_item_idx); +} + +static bool +all_children_complete(DragRemoteItem *parent) { + for (size_t i = 0; i < parent->children_sz; i++) { + if (!parent->children[i].completed) return false; + } + return true; +} + void drag_remote_file_data( Window *w, int32_t x, int32_t y, int32_t X, int32_t Y, bool has_more, const uint8_t *payload, size_t payload_sz ) { - size_t item_idx = ds.num_mimes; + size_t mime_item_idx = ds.num_mimes; for (size_t i = 0; i < ds.num_mimes; i++) { if (ds.items[i].requested_remote_files) { - item_idx = i; break; + mime_item_idx = i; break; } } - if (item_idx == ds.num_mimes || ds.items[item_idx].fd_plus_one == 0) abrt(EINVAL, "drag source remote file item index out of bounds"); - if (ds.items[item_idx].uri_list == NULL) { - ds.items[item_idx].uri_list = parse_uri_list(w, ds.items[item_idx].fd_plus_one-1, &ds.items[item_idx].num_uris); - if (!ds.items[item_idx].uri_list) return; + if (mime_item_idx == ds.num_mimes) abrt(EINVAL, "drag source no text/uri-list MIME entry data was requested"); + if (x < 1) abrt(EINVAL, "drag source remote item x index cannot be less than 1"); + const bool all_data_received = !payload_sz && !has_more; + const unsigned uri_item_idx = x - 1; + if (uri_item_idx >= mi.num_remote_items) abrt(EINVAL, "drag source uri list index out of bounds"); + DragRemoteItem *ri; + if (!Y) { + toplevel_data_for_drag(w, mime_item_idx, uri_item_idx, X, has_more, payload, payload_sz); + if (all_data_received) { + ri = mi.remote_items + uri_item_idx; + if (ri->waiting_for_completion && all_children_complete(ri)) { + ri->completed = true; + finish_remote_data_if_all_items_received(w, mime_item_idx); + } + } + } else { + if (y < 1) abrt(EINVAL, "drag source remote item y index cannot be less than 1"); + subdir_data_for_drag(w, mime_item_idx, x - 1, Y, y - 1, X, has_more, payload, payload_sz, &ri); + if (all_data_received && ri && all_children_complete(ri)) { + ri->completed = true; + while (1) { + ri = ri->parent; + if (ri) { + if (all_children_complete(ri)) ri->completed = true; + else break; + } else { + finish_remote_data_if_all_items_received(w, mime_item_idx); + break; + } + } + } } - if (X < 0) abrt(EINVAL, "drag source remote item X cannot be negative"); - if (!x && !y && !Y) { finish_remote_data(w, item_idx); return; } - if (!Y) toplevel_data_for_drag(w, item_idx, x - 1, X, has_more, payload, payload_sz); - else subdir_data_for_drag(w, item_idx, x - 1, Y, y - 1, X, has_more, payload, payload_sz); } #undef img #undef abrt #undef ds +#undef mi // }}} // DnD testing infrastructure {{{ @@ -2294,26 +2354,33 @@ dnd_test_force_drag_dropped(PyObject *self UNUSED, PyObject *args) { Window *w = window_for_window_id((id_type)window_id); if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; } // Simulate what drag_start does on success, without calling start_window_drag +#define ds w->drag_source for (size_t i = 0; i < w->drag_source.num_mimes; i++) { - free(w->drag_source.items[i].optional_data); - w->drag_source.items[i].optional_data = NULL; - w->drag_source.items[i].data_size = 0; - w->drag_source.items[i].data_capacity = 0; - w->drag_source.items[i].data_decode_initialized = false; + if (ds.is_remote_client && ds.items[i].is_uri_list) { + if (ds.items[i].optional_data && ds.items[i].data_size) { + ds.items[i].uri_list = parse_uri_list( + w, (char*)ds.items[i].optional_data, ds.items[i].data_size, &ds.items[i].num_uris); + } + } + free(ds.items[i].optional_data); + ds.items[i].optional_data = NULL; + ds.items[i].data_size = 0; + ds.items[i].data_capacity = 0; + ds.items[i].data_decode_initialized = false; } for (size_t i = 0; i < arraysz(w->drag_source.images); i++) { - if (w->drag_source.images[i].data) free(w->drag_source.images[i].data); - zero_at_ptr(w->drag_source.images + i); + if (ds.images[i].data) free(w->drag_source.images[i].data); + zero_at_ptr(ds.images + i); } - w->drag_source.state = DRAG_SOURCE_DROPPED; + ds.state = DRAG_SOURCE_DROPPED; +#undef ds Py_RETURN_NONE; } static PyObject * dnd_test_request_drag_data(PyObject *self UNUSED, PyObject *args) { // Simulate what drag_get_data does initially: find the MIME item at the - // given index, set requested_remote_files if appropriate, and return the - // escape code that would be sent to the client. + // given index, set requested_remote_files if appropriate. unsigned long long window_id; unsigned idx; if (!PyArg_ParseTuple(args, "KI", &window_id, &idx)) return NULL; @@ -2323,6 +2390,7 @@ dnd_test_request_drag_data(PyObject *self UNUSED, PyObject *args) { PyErr_SetString(PyExc_ValueError, "Invalid state or index"); return NULL; } w->drag_source.items[idx].requested_remote_files = w->drag_source.is_remote_client && w->drag_source.items[idx].is_uri_list; + if (w->drag_source.items[idx].requested_remote_files) request_remote_files(w, idx); Py_RETURN_NONE; } @@ -2420,6 +2488,19 @@ dnd_test_probe_state(PyObject *self UNUSED, PyObject *args) { if (strcmp(q, "drag_thumbnail_size") == 0) { return PyLong_FromSize_t(last_total_image_size); } + if (strcmp(q, "drag_remote_data_complete") == 0) { + for (size_t idx = 0; idx < w->drag_source.num_mimes; idx++) { +#define mi w->drag_source.items[idx] + if (mi.is_uri_list && mi.requested_remote_files) { + for (size_t i = 0; i < mi.num_remote_items; i++) { + if (mi.remote_items[i].waiting_for_completion && !mi.remote_items[i].completed) + return PyUnicode_FromString(mi.remote_items[i].dir_entry_name); + } + } +#undef mi + } + Py_RETURN_NONE; + } Py_RETURN_NONE; } diff --git a/kitty/state.h b/kitty/state.h index 0a9afc2fb..a4e65a49d 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -253,7 +253,8 @@ typedef struct DragRemoteItem { size_t children_sz; char *dir_entry_name; base64_state base64_state; - bool started; + bool started, waiting_for_completion, completed; + struct DragRemoteItem *parent; } DragRemoteItem; typedef struct Window { diff --git a/kitty_tests/__init__.py b/kitty_tests/__init__.py index 759792d22..8c2c4a602 100644 --- a/kitty_tests/__init__.py +++ b/kitty_tests/__init__.py @@ -426,7 +426,7 @@ class PTY: if isinstance(data, str): data = data.encode('utf-8') if self.log_data_flow: - print('t -> c:', data) + print('t -> c:', bytes(data)) self.write_buf += data if flush: self.process_input_from_child(0) diff --git a/kitty_tests/dnd.py b/kitty_tests/dnd.py index 6eb449434..9381c4c1c 100644 --- a/kitty_tests/dnd.py +++ b/kitty_tests/dnd.py @@ -18,6 +18,7 @@ from kitty.fast_data_types import ( dnd_test_fake_drop_data, dnd_test_fake_drop_event, dnd_test_force_drag_dropped, + dnd_test_probe_state, dnd_test_request_drag_data, dnd_test_set_mouse_pos, ) @@ -236,14 +237,6 @@ def client_remote_file( return _osc(meta) -def client_remote_file_finish(client_id: int = 0) -> bytes: - """Escape code signaling completion of all remote file data (t=k with no keys).""" - meta = f'{DND_CODE};t=k' - if client_id: - meta += f':i={client_id}' - return _osc(meta) - - # ---- escape-code decoder used by assertions --------------------------------- _OSC_RE = re.compile( @@ -2340,18 +2333,19 @@ class TestDnDProtocol(BaseTest): # Register with a different machine_id to make is_remote_client=True parse_bytes(screen, _osc(f'{DND_CODE};t=o:x=1;different-machine-id')) parse_bytes(screen, client_drag_offer_mimes(operations, mimes, client_id=client_id)) - cap.consume() - dnd_test_force_drag_dropped(cap.window_id) - # Find the index of text/uri-list mime_list = mimes.split() uri_idx = mime_list.index('text/uri-list') - dnd_test_request_drag_data(cap.window_id, uri_idx) - # Send the uri-list data b64 = standard_b64encode(uri_list_data).decode() - parse_bytes(screen, client_drag_send_data(uri_idx, b64, client_id=client_id)) - # End of data - parse_bytes(screen, client_drag_send_data(uri_idx, '', client_id=client_id)) + parse_bytes(screen, client_drag_pre_send(uri_idx, b64, client_id=client_id)) cap.consume() + dnd_test_force_drag_dropped(cap.window_id) + dnd_test_request_drag_data(cap.window_id, uri_idx) + events = self._get_events(cap) + expected = 0 + for line in uri_list_data.decode().splitlines(): + if line.startswith('file://'): + expected += 1 + self.assertEqual(expected, len(events)) def test_remote_drag_single_file(self) -> None: """Transfer a single regular file via t=k.""" @@ -2366,9 +2360,7 @@ class TestDnDProtocol(BaseTest): # End of data for this file parse_bytes(screen, client_remote_file(1, '', item_type=0)) self._assert_no_output(cap) - # Completion signal - parse_bytes(screen, client_remote_file_finish()) - self._assert_no_output(cap) + self.assert_drag_data_complete(cap) def test_remote_drag_single_symlink(self) -> None: """Transfer a symlink via t=k with X=1.""" @@ -2383,9 +2375,7 @@ class TestDnDProtocol(BaseTest): # End of data parse_bytes(screen, client_remote_file(1, '', item_type=1)) self._assert_no_output(cap) - # Completion signal - parse_bytes(screen, client_remote_file_finish()) - self._assert_no_output(cap) + self.assert_drag_data_complete(cap) def test_remote_drag_single_directory(self) -> None: """Transfer a directory with entries via t=k with X=handle (>1).""" @@ -2422,10 +2412,7 @@ class TestDnDProtocol(BaseTest): parse_bytes(screen, client_remote_file( 1, '', item_type=0, parent_handle=2, entry_num=2)) self._assert_no_output(cap) - - # Completion signal - parse_bytes(screen, client_remote_file_finish()) - self._assert_no_output(cap) + self.assert_drag_data_complete(cap) def test_remote_drag_multiple_uris(self) -> None: """Transfer multiple files from a URI list.""" @@ -2442,9 +2429,7 @@ class TestDnDProtocol(BaseTest): parse_bytes(screen, client_remote_file(2, b64, item_type=0)) parse_bytes(screen, client_remote_file(2, '', item_type=0)) self._assert_no_output(cap) - # Completion - parse_bytes(screen, client_remote_file_finish()) - self._assert_no_output(cap) + self.assert_drag_data_complete(cap) def test_remote_drag_chunked_file(self) -> None: """File data can be sent in multiple chunks with m=1.""" @@ -2468,9 +2453,7 @@ class TestDnDProtocol(BaseTest): # End of data parse_bytes(screen, client_remote_file(1, '', item_type=0)) self._assert_no_output(cap) - # Completion - parse_bytes(screen, client_remote_file_finish()) - self._assert_no_output(cap) + self.assert_drag_data_complete(cap) def test_remote_drag_directory_with_symlink(self) -> None: """Directory can contain symlinks (X=1 type for children).""" @@ -2499,10 +2482,7 @@ class TestDnDProtocol(BaseTest): parse_bytes(screen, client_remote_file( 1, '', item_type=1, parent_handle=2, entry_num=2)) self._assert_no_output(cap) - - # Completion - parse_bytes(screen, client_remote_file_finish()) - self._assert_no_output(cap) + self.assert_drag_data_complete(cap) def test_remote_drag_deep_directory_tree_breadth_first(self) -> None: """Transfer a 3-level deep directory tree in breadth-first order. @@ -2572,10 +2552,7 @@ class TestDnDProtocol(BaseTest): 1, b64, item_type=1, parent_handle=4, entry_num=2)) parse_bytes(screen, client_remote_file( 1, '', item_type=1, parent_handle=4, entry_num=2)) - - # Completion - parse_bytes(screen, client_remote_file_finish()) - self._assert_no_output(cap) + self.assert_drag_data_complete(cap) def test_remote_drag_deep_directory_tree_depth_first(self) -> None: """Transfer a 3-level deep directory tree in depth-first order. @@ -2645,8 +2622,7 @@ class TestDnDProtocol(BaseTest): parse_bytes(screen, client_remote_file( 1, '', item_type=1, parent_handle=4, entry_num=2)) - # Completion - parse_bytes(screen, client_remote_file_finish()) + self.assert_drag_data_complete(cap) self._assert_no_output(cap) def test_remote_drag_completion_signal(self) -> None: @@ -2657,8 +2633,7 @@ class TestDnDProtocol(BaseTest): b64 = standard_b64encode(b'data').decode() parse_bytes(screen, client_remote_file(1, b64, item_type=0)) parse_bytes(screen, client_remote_file(1, '', item_type=0)) - # Completion - parse_bytes(screen, client_remote_file_finish()) + self.assert_drag_data_complete(cap) self._assert_no_output(cap) def test_remote_drag_invalid_uri_index(self) -> None: @@ -2726,15 +2701,6 @@ class TestDnDProtocol(BaseTest): parse_bytes(screen, client_remote_file(1, big_b64, item_type=0)) self.assert_error(cap) - def test_remote_drag_negative_X_rejected(self) -> None: - """Sending t=k with X < 0 is rejected.""" - uri_list = b'file:///home/user/f.txt\r\n' - with dnd_test_window() as (screen, cap): - self._setup_remote_drag(screen, cap, uri_list) - # Directly construct escape code with negative X - parse_bytes(screen, _osc(f'{DND_CODE};t=k:x=1:X=-1')) - self.assert_error(cap) - def test_remote_drag_without_remote_flag_fails(self) -> None: """t=k fails if the drag is not from a remote client.""" with dnd_test_window() as (screen, cap): @@ -2780,7 +2746,7 @@ class TestDnDProtocol(BaseTest): def test_remote_drag_dos_present_data_cap_on_directory(self) -> None: """Directory listing data exceeding PRESENT_DATA_CAP triggers EMFILE error.""" uri_list = b'file:///home/user/dir\r\n' - with dnd_test_window(present_data_cap=20) as (screen, cap): + with dnd_test_window(present_data_cap=100) as (screen, cap): self._setup_remote_drag(screen, cap, uri_list) # Send a directory listing that will exceed the cap big_listing = b'\x00'.join([f'file{i}.txt'.encode() for i in range(100)]) @@ -2801,6 +2767,10 @@ class TestDnDProtocol(BaseTest): parse_bytes(screen, client_remote_file(1, b64, item_type=0)) self.assert_error(cap) + def assert_drag_data_complete(self, cap) -> None: + first_incomplete_entry = dnd_test_probe_state(cap.window_id, 'drag_remote_data_complete') + self.assertIsNone(first_incomplete_entry) + def test_remote_drag_three_level_tree_with_verification(self) -> None: """Transfer a 3-level directory tree and verify no errors occur. @@ -2875,9 +2845,7 @@ class TestDnDProtocol(BaseTest): 1, '', item_type=1, parent_handle=30, entry_num=2)) self._assert_no_output(cap) - # Completion - parse_bytes(screen, client_remote_file_finish()) - self._assert_no_output(cap) + self.assert_drag_data_complete(cap) def test_remote_drag_process_item_data_basic(self) -> None: """Basic drag_process_item_data: send data for a MIME type after DROPPED state.""" @@ -2954,8 +2922,7 @@ class TestDnDProtocol(BaseTest): parse_bytes(screen, client_remote_file(3, '', item_type=1)) self._assert_no_output(cap) - parse_bytes(screen, client_remote_file_finish()) - self._assert_no_output(cap) + self.assert_drag_data_complete(cap) def test_remote_drag_empty_file(self) -> None: """Transfer an empty file (end-of-data immediately after start).""" @@ -2965,8 +2932,7 @@ class TestDnDProtocol(BaseTest): # Start file transfer, then immediately end (no data chunks) parse_bytes(screen, client_remote_file(1, '', item_type=0)) self._assert_no_output(cap) - parse_bytes(screen, client_remote_file_finish()) - self._assert_no_output(cap) + self.assert_drag_data_complete(cap) def test_remote_drag_empty_directory(self) -> None: """Transfer a directory with no entries.""" @@ -2977,8 +2943,7 @@ class TestDnDProtocol(BaseTest): b64 = standard_b64encode(b'').decode() parse_bytes(screen, client_remote_file(1, b64, item_type=2)) parse_bytes(screen, client_remote_file(1, '', item_type=2)) - parse_bytes(screen, client_remote_file_finish()) - self._assert_no_output(cap) + self.assert_drag_data_complete(cap) def test_remote_drag_uri_list_with_comments(self) -> None: """URI list with comment lines (starting with #) should filter them out.""" @@ -3027,9 +2992,7 @@ class TestDnDProtocol(BaseTest): parse_bytes(screen, client_remote_file( 1, '', item_type=0, parent_handle=2, entry_num=2)) self._assert_no_output(cap) - - parse_bytes(screen, client_remote_file_finish()) - self._assert_no_output(cap) + self.assert_drag_data_complete(cap) # ---- DoS limits tests --------------------------------------------------- @@ -3137,9 +3100,6 @@ class TestDnDProtocol(BaseTest): # End of data for this file parse_bytes(screen, client_remote_file(1, '', item_type=0)) self._assert_no_output(cap) - # Completion signal - parse_bytes(screen, client_remote_file_finish()) - self._assert_no_output(cap) # No crash or leak - cleanup happens in context manager exit # ---- query tests --------------------------------------------------------