summaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/terminal/snapshot/envelope.zig
blob: 21fcefb9d284f4edec94952db5a6340a36f31449 (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
//! Snapshot envelope.
//!
//! Every snapshot binary blob begins with exactly one envelope at byte zero.
//! It is followed by a set of records. Each record provides their own
//! tag, payload length, CRC, etc.
//!
//! The envelope identifies the bytes as a terminal snapshot and selects the
//! single version governing the entire blob. A decoder validates both fields
//! before reading any records.
//!
//! The envelope is exactly ten bytes. All integers are unsigned and
//! little-endian.
//!
//! | Offset | Size | Field                |
//! | -----: | ---: | :------------------- |
//! |      0 |    8 | Magic (`GHOSTSNP`)   |
//! |      8 |    2 | Version (`u16`)      |

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

/// Identifies a Ghostty terminal snapshot and rejects unrelated input before
/// any record decoding begins. All eight bytes are part of the wire value.
pub const magic = "GHOSTSNP";

/// The complete compatibility boundary for snapshot layout and behavior.
/// Version 1 readers require this value to match exactly.
pub const version: u16 = 1;

/// Number of bytes in the fixed envelope: magic followed by version.
pub const encoded_len = computeLen();

comptime {
    // We expect this so if it changes we should think carefully.
    std.debug.assert(encoded_len == 10);
}

pub const DecodeError = std.Io.Reader.Error || error{
    InvalidMagic,
    UnsupportedVersion,
};

/// Encode the envelope.
pub fn encode(writer: *std.Io.Writer) std.Io.Writer.Error!void {
    try writer.writeAll(magic);
    try io.writeInt(writer, u16, version);
}

/// Decode and validate the envelope.
pub fn decode(reader: *std.Io.Reader) DecodeError!void {
    var actual_magic: [magic.len]u8 = undefined;
    try reader.readSliceAll(&actual_magic);
    if (!std.mem.eql(u8, magic, &actual_magic)) return error.InvalidMagic;

    const actual_version = try io.readInt(reader, u16);
    if (actual_version != version) return error.UnsupportedVersion;
}

fn computeLen() usize {
    comptime {
        var buf: [128]u8 = undefined;
        var writer: std.Io.Writer = .fixed(&buf);
        encode(&writer) catch unreachable;
        return writer.end;
    }
}

const test_golden_fixture = test_fixture.parse(@embedFile("testdata/envelope-v1.hex"));

test "golden encoding" {
    var buf: [encoded_len]u8 = undefined;
    var writer: std.Io.Writer = .fixed(&buf);
    try encode(&writer);

    try test_fixture.expectEqual(
        .bytes,
        "src/terminal/snapshot/testdata/envelope-v1.hex",
        "snapshot_fixture-envelope-v1.hex",
        &test_golden_fixture,
        writer.buffered(),
    );

    var reader: std.Io.Reader = .fixed(&test_golden_fixture);
    try decode(&reader);
}

test "reject invalid magic and version" {
    var invalid_magic: std.Io.Reader = .fixed("GHOSTSNX\x01\x00");
    try std.testing.expectError(error.InvalidMagic, decode(&invalid_magic));

    var invalid_version: std.Io.Reader = .fixed("GHOSTSNP\x00\x00");
    try std.testing.expectError(
        error.UnsupportedVersion,
        decode(&invalid_version),
    );
}

test "reject every truncation" {
    for (0..encoded_len) |len| {
        var reader: std.Io.Reader = .fixed(test_golden_fixture[0..len]);
        try std.testing.expectError(error.EndOfStream, decode(&reader));
    }
}