diff --git a/src/terminal/snapshot/page.zig b/src/terminal/snapshot/page.zig index 0d166e389..bd64e483e 100644 --- a/src/terminal/snapshot/page.zig +++ b/src/terminal/snapshot/page.zig @@ -233,7 +233,17 @@ pub const Decoder = struct { // payload, so this is the byte count of the tables and grid. const remaining = self.record_reader.header.payload_len - Header.len; - if (remaining <= max_staged_payload) { + if (self.record_reader.payloadReader().bufferedLen() >= remaining) { + // The complete payload is already buffered, e.g. borrowed from + // an in-memory snapshot. Parse it in place with no staging + // copy; `finish` still enforces the CRC and exact exhaustion. + try decodePayloadBody( + self.record_reader.payloadReader(), + alloc, + destination, + self.header, + ); + } else if (remaining <= max_staged_payload) { // Stage the payload with one bulk read. The whole payload passes // through the checksum hasher as one update and the payload // decoders then parse a flat buffer, which keeps per-row work free diff --git a/src/terminal/snapshot/record.zig b/src/terminal/snapshot/record.zig index e8dada911..397d00cbe 100644 --- a/src/terminal/snapshot/record.zig +++ b/src/terminal/snapshot/record.zig @@ -257,6 +257,18 @@ pub const Reader = struct { limited: std.Io.Reader.Limited, hashing: std.Io.Reader.Hashed(Crc32c), + /// When the source already has the complete payload buffered, for + /// example an in-memory snapshot, the payload is borrowed straight + /// from the source buffer instead of streaming through the limited + /// and hashing adapters. The checksum is then verified with one bulk + /// update in `finish`, and the source is not advanced until `finish`. + borrowed: ?Borrowed, + + const Borrowed = struct { + source: *std.Io.Reader, + payload: std.Io.Reader, + }; + pub const InitError = Header.DecodeError; /// Errors detected after a payload decoder returns. @@ -278,6 +290,21 @@ pub const Reader = struct { ) InitError!void { self.* = undefined; self.header = try Header.decode(source); + + // The complete payload is already sitting in the source buffer: + // borrow it in place. Payload decoders read from a fixed reader + // over the borrowed bytes and `finish` checksums them in one pass. + if (source.bufferedLen() >= self.header.payload_len) { + self.borrowed = .{ + .source = source, + .payload = .fixed( + source.buffered()[0..self.header.payload_len], + ), + }; + return; + } + + self.borrowed = null; self.limited = .init( source, .limited(self.header.payload_len), @@ -297,11 +324,34 @@ pub const Reader = struct { /// Return the length-limited, checksum-updating payload reader. pub fn payloadReader(self: *Reader) *std.Io.Reader { + if (self.borrowed) |*borrowed| return &borrowed.payload; return &self.hashing.reader; } /// Require exact payload exhaustion and validate its CRC32C. pub fn finish(self: *Reader) FinishError!void { + if (self.borrowed) |*borrowed| { + if (borrowed.payload.bufferedLen() != 0) { + return error.PayloadNotExhausted; + } + + var checksum: Checksum = .init( + self.header.tag, + self.header.payload_len, + ); + checksum.writer().writeAll( + borrowed.payload.buffer[0..borrowed.payload.end], + ) catch unreachable; + if (checksum.final() != self.header.crc32c) { + return error.InvalidChecksum; + } + + // The borrowed bytes validated, so consume them from the + // source only now, leaving it positioned at the next record. + borrowed.source.toss(self.header.payload_len); + return; + } + if (self.hashing.reader.bufferedLen() != 0 or self.limited.remaining != .nothing) {