diff --git a/client.go b/client.go index 38167ba..bc41795 100644 --- a/client.go +++ b/client.go @@ -260,6 +260,13 @@ func (c *Client) recvLoop() { defer c.wg.Done() buf := make([]byte, maxPayload+64) + // Track the base state (oldNum) of the most recently buffered output. + // Mosh diffs are cumulative snapshots from a given base. When a new diff + // has the same oldNum as the previous one, it supersedes the older diff + // and we replace the output buffer instead of appending. This prevents + // character ghosting caused by concatenating overlapping diffs. + var lastOutputOldNum uint64 + for { select { case <-c.done: @@ -287,6 +294,9 @@ func (c *Client) recvLoop() { continue } + // Read oldNum AFTER transport.Recv() — it's set by the Recv call above. + currentOldNum := c.transport.LastRecvOldNum() + instrs, err := UnmarshalHostMessage(diff) if err != nil || len(instrs) == 0 { continue @@ -301,7 +311,14 @@ func (c *Client) recvLoop() { } c.mu.Lock() - c.output = append(c.output, output...) + // Same base → newer diff supersedes older one (replace). + // Different base → new incremental content (append). + if currentOldNum == lastOutputOldNum { + c.output = output + } else { + c.output = append(c.output, output...) + } + lastOutputOldNum = currentOldNum c.mu.Unlock() select { diff --git a/go.mod b/go.mod index d101575..6021fc5 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/unixshells/mosh-go +module github.com/ys-ll/mosh-go go 1.26.0 diff --git a/transport.go b/transport.go index 8ba7473..a659fc5 100644 --- a/transport.go +++ b/transport.go @@ -63,10 +63,6 @@ type Transport struct { // Latch capability negotiation. localCaps []byte remoteCaps []byte - - // Reusable zlib writer to avoid per-tick allocations. - zlibBuf bytes.Buffer - zlibW *zlib.Writer } const ( @@ -187,7 +183,7 @@ func (t *Transport) Tick() [][]byte { // Marshal → compress → fragment → encrypt. pbData := ti.Marshal() - compressed := t.zlibCompress(pbData) + compressed := zlibCompress(pbData) frags := Fragmentize(t.sentNum, compressed) var datagrams [][]byte @@ -308,15 +304,12 @@ func (t *Transport) Recv(wire []byte) []byte { } } - // Check if we have old_num (required to apply diff). - hasOld := false - for _, n := range t.receivedNums { - if n == ti.OldNum { - hasOld = true - break - } - } - if !hasOld { + // Reject diffs that do not chain from our latest acknowledged state. + // Mosh diffs MUST be sequential: each diff's oldNum must equal the + // newNum of the most recently accepted diff. Accepting diffs from + // stale bases (oldNum != ackNum) causes overlapping ANSI sequences + // that produce character ghosting and garbled output. + if ti.OldNum != t.ackNum { return nil } @@ -473,19 +466,13 @@ func (t *Transport) updateRTT(tsReply uint16) { } } -// zlibCompress compresses data with zlib, reusing the writer. -func (t *Transport) zlibCompress(data []byte) []byte { - t.zlibBuf.Reset() - if t.zlibW == nil { - t.zlibW = zlib.NewWriter(&t.zlibBuf) - } else { - t.zlibW.Reset(&t.zlibBuf) - } - t.zlibW.Write(data) - t.zlibW.Close() - out := make([]byte, t.zlibBuf.Len()) - copy(out, t.zlibBuf.Bytes()) - return out +// zlibCompress compresses data with zlib (default level). +func zlibCompress(data []byte) []byte { + var buf bytes.Buffer + w := zlib.NewWriter(&buf) + w.Write(data) + w.Close() + return buf.Bytes() } // zlibDecompress decompresses zlib data. Returns nil on error.