summaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/terminal/snapshot/checkpoint.zig
blob: 23ea755f8683a7465533d54854c356e1e7a860cf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
//! READY and FINISH snapshot marker records.
//!
//! READY separates the renderable terminal state from the history sequences.
//! FINISH terminates one snapshot; bytes after it belong to the containing
//! transport. Both markers have empty payloads, independently framed and
//! protected by the same CRC32C as every other record.
//!
//! ## Binary Format
//!
//! READY and FINISH contain only their record headers. Their declared payload
//! length must be zero.

const std = @import("std");
const test_fixture = @import("fixture.zig");
const record = @import("record.zig");

/// Selects one of the two marker positions in a snapshot.
pub const Kind = enum {
    ready,
    finish,

    fn tag(self: Kind) record.Tag {
        return switch (self) {
            .ready => .ready,
            .finish => .finish,
        };
    }
};

pub const EncodeError = record.Writer.FinishError;

/// Append one empty marker record.
pub fn encode(kind: Kind, stream: *record.Writer) EncodeError!void {
    _ = stream.begin(kind.tag());
    errdefer stream.cancel();
    try stream.finish();
}

pub const DecodeError = record.Reader.InitError ||
    record.Reader.FinishError ||
    error{
        /// The next record is valid but is not the expected marker.
        UnexpectedRecordTag,
    };

/// Decode one marker and require its payload to be empty.
pub fn decode(
    kind: Kind,
    source: *std.Io.Reader,
) DecodeError!void {
    var record_reader: record.Reader = undefined;
    try record_reader.init(source);
    if (record_reader.header.tag != kind.tag()) {
        return error.UnexpectedRecordTag;
    }
    try record_reader.finish();
}

const test_ready_fixture = test_fixture.parse(
    @embedFile("testdata/checkpoint-ready-v1.hex"),
);

test "READY golden encoding" {
    const prefix = "abc";

    var snapshot: std.Io.Writer.Allocating = .init(std.testing.allocator);
    defer snapshot.deinit();
    var stream: record.Writer = .init(
        std.testing.allocator,
        &snapshot.writer,
    );
    defer stream.deinit();
    try stream.writer().writeAll(prefix);
    try encode(.ready, &stream);

    try test_fixture.expectEqual(
        .bytes,
        "src/terminal/snapshot/testdata/checkpoint-ready-v1.hex",
        "snapshot_fixture-checkpoint-ready-v1.hex",
        &test_ready_fixture,
        snapshot.written(),
    );

    // Decode the checked-in record rather than the generated candidate.
    var source: std.Io.Reader = .fixed(&test_ready_fixture);
    try source.discardAll(prefix.len);
    try decode(.ready, &source);
}

test "READY and FINISH are empty marker records" {
    const testing = std.testing;

    var snapshot: std.Io.Writer.Allocating = .init(testing.allocator);
    defer snapshot.deinit();
    var stream: record.Writer = .init(
        testing.allocator,
        &snapshot.writer,
    );
    defer stream.deinit();
    try stream.writer().writeAll("prefix");

    const ready_offset = snapshot.written().len;
    try encode(.ready, &stream);
    try stream.writer().writeAll("history");
    const finish_offset = snapshot.written().len;
    try encode(.finish, &stream);

    try testing.expectEqual(
        record.Header.len,
        finish_offset - ready_offset - "history".len,
    );
    try testing.expectEqual(
        record.Header.len,
        snapshot.written().len - finish_offset,
    );

    var source: std.Io.Reader = .fixed(snapshot.written());
    try source.discardAll(ready_offset);
    try decode(.ready, &source);
    try source.discardAll("history".len);
    try decode(.finish, &source);
}

test "checkpoint rejects wrong tags and nonempty payloads" {
    const testing = std.testing;

    var snapshot: std.Io.Writer.Allocating = .init(testing.allocator);
    defer snapshot.deinit();
    var stream: record.Writer = .init(
        testing.allocator,
        &snapshot.writer,
    );
    defer stream.deinit();
    try encode(.ready, &stream);

    var wrong_tag_source: std.Io.Reader = .fixed(snapshot.written());
    try testing.expectError(
        error.UnexpectedRecordTag,
        decode(.finish, &wrong_tag_source),
    );

    var invalid: std.Io.Writer.Allocating = .init(testing.allocator);
    defer invalid.deinit();
    var invalid_stream: record.Writer = .init(
        testing.allocator,
        &invalid.writer,
    );
    defer invalid_stream.deinit();
    const invalid_payload = invalid_stream.begin(.ready);
    errdefer invalid_stream.cancel();
    try invalid_payload.writeByte(0);
    try invalid_stream.finish();

    var invalid_source: std.Io.Reader = .fixed(invalid.written());
    try testing.expectError(
        error.PayloadNotExhausted,
        decode(.ready, &invalid_source),
    );
}

test "FINISH leaves continuation bytes unread" {
    const testing = std.testing;

    var finished: std.Io.Writer.Allocating = .init(testing.allocator);
    defer finished.deinit();
    var finished_stream: record.Writer = .init(
        testing.allocator,
        &finished.writer,
    );
    defer finished_stream.deinit();
    try encode(.finish, &finished_stream);
    try finished.writer.writeByte(0);

    var source: std.Io.Reader = .fixed(finished.written());
    try decode(.finish, &source);
    try testing.expectEqual(@as(u8, 0), try source.takeByte());
}