diff --git a/lib/model/folder_sendrecv.go b/lib/model/folder_sendrecv.go index 7d7238b19..c13024f52 100644 --- a/lib/model/folder_sendrecv.go +++ b/lib/model/folder_sendrecv.go @@ -1345,6 +1345,12 @@ func (f *sendReceiveFolder) copierRoutine(ctx context.Context, in <-chan copyBlo default: } + if block.Size == 0 { + // Copying zero bytes is a no-op. + state.copyDone(block) + continue + } + if !f.DisableSparseFiles && state.reused == 0 && block.IsEmpty() { // The block is a block of all zeroes, and we are not reusing // a temp file, so there is no need to do anything with it. @@ -1534,6 +1540,13 @@ func (f *sendReceiveFolder) pullerRoutine(ctx context.Context, in <-chan pullBlo bytes := state.block.Size + if bytes == 0 { + // Pulling zero bytes is a no-op. + state.pullDone(state.block) + out <- state.sharedPullerState + continue + } + if err := requestLimiter.TakeWithContext(ctx, bytes); err != nil { state.fail(err) out <- state.sharedPullerState diff --git a/lib/model/folder_sendrecv_test.go b/lib/model/folder_sendrecv_test.go index 8bd56b088..1daba2c5c 100644 --- a/lib/model/folder_sendrecv_test.go +++ b/lib/model/folder_sendrecv_test.go @@ -891,7 +891,7 @@ func TestPullCtxCancel(t *testing.T) { emptyState := func() pullBlockState { return pullBlockState{ sharedPullerState: newSharedPullerState(protocol.FileInfo{}, nil, f.folderID, "", nil, nil, false, false, protocol.FileInfo{}, false, false), - block: protocol.BlockInfo{}, + block: protocol.BlockInfo{Size: 42}, } } diff --git a/lib/protocol/protocol.go b/lib/protocol/protocol.go index fa4746cbe..077cc2702 100644 --- a/lib/protocol/protocol.go +++ b/lib/protocol/protocol.go @@ -483,7 +483,7 @@ func (c *rawConnection) dispatcherLoop() (err error) { if err := checkFilename(msg.Name); err != nil { return newProtocolError(err, msgContext) } - if msg.Size <= 0 { + if msg.Size < 0 { return newProtocolError(fmt.Errorf("request size %d too small", msg.Size), msgContext) } if msg.Size > MaxRequestSize { diff --git a/lib/protocol/protocol_test.go b/lib/protocol/protocol_test.go index 7f3d23f1b..743312ad4 100644 --- a/lib/protocol/protocol_test.go +++ b/lib/protocol/protocol_test.go @@ -544,7 +544,7 @@ func TestDispatcherToCloseDeadlock(t *testing.T) { } func TestRequestMaxSize(t *testing.T) { - invalidSize := []int{-65536, 0, MaxRequestSize + 1} + invalidSize := []int{-65536, -1, MaxRequestSize + 1} for _, s := range invalidSize { t.Run(fmt.Sprintf("invalid/%d", s), func(t *testing.T) { m := newTestModel() @@ -592,6 +592,34 @@ func TestRequestMaxSize(t *testing.T) { } } +func TestRequestZeroSize(t *testing.T) { + // A zero-sized request should be accepted, since current versions of + // Syncthing send these. See https://github.com/syncthing/syncthing/issues/10709. + m := newTestModel() + rw := testutil.NewBlockingRW() + c := getRawConnection(NewConnection(c0ID, rw, &testutil.NoopRW{}, testutil.NoopCloser{}, m, new(mockedConnectionInfo), CompressionAlways, testKeyGen)) + c.Start() + defer closeAndWait(c, rw) + + c.inbox <- &bep.ClusterConfig{} + c.inbox <- &bep.Request{ + Id: 1, + Name: "valid", + Size: 0, + } + + select { + case res := <-c.outbox: + if msg, ok := res.msg.(*bep.Response); !ok || msg.Id != 1 { + t.Errorf("bad response %#v", msg) + } + case <-c.dispatcherLoopStopped: + t.Fatal("dispatcher loop terminated, expected zero-sized request to be accepted") + case <-time.After(time.Second): + t.Fatal("timed out waiting for response") + } +} + func TestRequestInvalidFilename(t *testing.T) { m := newTestModel() rw := testutil.NewBlockingRW()