terminal/snapshot: report invalid decoder states

Decoder.next treated calls before READY and calls after any prior decode
error as unreachable. Network or mux glue that retried after a truncated
history record, or invoked next before setup completed, could therefore
turn a recoverable protocol misuse into a process panic.

Add DecoderNotReady and DecoderFailed to NextError and return them for
the start and failed states. Keep finished calls idempotent, and cover
both an early call and a retry after FINISH truncation.
This commit is contained in:
Mitchell Hashimoto
2026-08-02 20:48:22 -07:00
parent d148471838
commit cbc9f360b1

View File

@@ -413,6 +413,12 @@ pub const Decoder = struct {
page.DecodeError ||
Allocator.Error ||
error{
/// `next` was called before `ready` completed successfully.
DecoderNotReady,
/// A prior `next` call failed and invalidated the stream position.
DecoderFailed,
/// A HISTORY names a key not declared by TERMINAL.
UnexpectedHistoryKey,
@@ -457,7 +463,8 @@ pub const Decoder = struct {
switch (self.state) {
// `ready` must succeed before history exists to decode, and a
// failed decoder no longer knows its stream position.
.start, .failed => unreachable,
.start => return error.DecoderNotReady,
.failed => return error.DecoderFailed,
.finished => return null,
.history => {},
}
@@ -1774,6 +1781,23 @@ test "incremental decode restores a renderable terminal at READY" {
try testing.expectEqualStrings(&test_complete_fixture, reencoded.written());
}
test "incremental next reports invalid decoder states" {
const testing = std.testing;
var source: std.Io.Reader = .fixed("");
var decoder: Decoder = .init(&source);
var terminal_value = try Terminal.init(
testing.io,
testing.allocator,
.{ .cols = 1, .rows = 1 },
);
defer terminal_value.deinit(testing.allocator);
try testing.expectError(
error.DecoderNotReady,
decoder.next(testing.allocator, &terminal_value),
);
}
test "incremental decode applies history below live PTY output" {
const testing = std.testing;
@@ -2065,6 +2089,10 @@ test "incremental decode failure leaves applied history usable" {
error.EndOfStream,
decoder.next(testing.allocator, &restored),
);
try testing.expectError(
error.DecoderFailed,
decoder.next(testing.allocator, &restored),
);
// The transferred terminal was never owned by the decoder: it remains
// valid with the contiguous history prefix that did apply, and only