diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/crash/sentry_envelope.zig | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (diff) | |
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/crash/sentry_envelope.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/crash/sentry_envelope.zig | 597 |
1 files changed, 597 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/crash/sentry_envelope.zig b/vendor/libghostty-vt/src/crash/sentry_envelope.zig new file mode 100644 index 00000000..08573b73 --- /dev/null +++ b/vendor/libghostty-vt/src/crash/sentry_envelope.zig @@ -0,0 +1,597 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const log = std.log.scoped(.sentry_envelope); + +/// The Sentry Envelope format: https://develop.sentry.dev/sdk/envelopes/ +/// +/// The envelope is our primary crash report format since use the Sentry +/// client. It is designed and created by Sentry but is an open format +/// in that it is publicly documented and can be used by any system. This +/// lets us utilize the Sentry client for crash capture but also gives us +/// the opportunity to migrate to another system if we need to, and doesn't +/// force any user or developer to use Sentry the SaaS if they don't want +/// to. +/// +/// This struct implements reading the envelope format (writing is not needed +/// currently but can be added later). It is incomplete; I only implemented +/// what I needed at the time. +pub const Envelope = struct { + /// The arena that the envelope is allocated in. All items are welcome + /// to use this allocator for their data, which is freed on deinit. + arena: std.heap.ArenaAllocator, + + /// The headers of the envelope decoded into a json ObjectMap. + headers: std.json.ObjectMap, + + /// The items in the envelope in the order they're encoded. + items: std.ArrayList(Item), + + /// Parse an envelope from a reader. + /// + /// The full envelope must fit in memory for this to succeed. This + /// will always copy the data from the reader into memory, even if the + /// reader is already in-memory (i.e. a FixedBufferStream). This + /// simplifies memory lifetimes at the expense of a copy, but envelope + /// parsing in our use case is not a hot path. + pub fn parse( + alloc_gpa: Allocator, + reader: *std.Io.Reader, + ) !Envelope { + // We use an arena allocator to read from reader. We pair this + // with `alloc_if_needed` when parsing json to allow the json + // to reference the arena-allocated memory if it can. That way both + // our temp and perm memory is part of the same arena. This slightly + // bloats our memory requirements but reduces allocations. + var arena = std.heap.ArenaAllocator.init(alloc_gpa); + errdefer arena.deinit(); + const alloc = arena.allocator(); + + // Parse our elements. We do this outside of the struct assignment + // below to avoid the issue where order matters in struct assignment. + const headers = try parseHeader(alloc, reader); + const items = try parseItems(alloc, reader); + + return .{ + .headers = headers, + .items = items, + .arena = arena, + }; + } + + fn parseHeader( + alloc: Allocator, + reader: *std.Io.Reader, + ) !std.json.ObjectMap { + var buf: std.Io.Writer.Allocating = .init(alloc); + _ = try reader.streamDelimiterLimit( + &buf.writer, + '\n', + .limited(1024 * 1024), // 1MB, arbitrary choice + ); + _ = reader.discardDelimiterInclusive('\n') catch |err| switch (err) { + // It's okay if there isn't a trailing newline + error.EndOfStream => {}, + else => return err, + }; + + const value = try std.json.parseFromSliceLeaky( + std.json.Value, + alloc, + buf.written(), + .{ .allocate = .alloc_if_needed }, + ); + + return switch (value) { + .object => |map| map, + else => error.EnvelopeMalformedHeaders, + }; + } + + fn parseItems( + alloc: Allocator, + reader: *std.Io.Reader, + ) !std.ArrayList(Item) { + var items: std.ArrayList(Item) = .{}; + errdefer items.deinit(alloc); + while (try parseOneItem(alloc, reader)) |item| { + try items.append(alloc, item); + } + + return items; + } + + fn parseOneItem( + alloc: Allocator, + reader: *std.Io.Reader, + ) !?Item { + // Get the next item which must start with a header. + var buf: std.Io.Writer.Allocating = .init(alloc); + _ = reader.streamDelimiterLimit( + &buf.writer, + '\n', + .limited(1024 * 1024), // 1MB, arbitrary choice + ) catch |err| switch (err) { + error.StreamTooLong => return null, + else => return err, + }; + _ = reader.discardDelimiterInclusive('\n') catch |err| switch (err) { + // It's okay if there isn't a trailing newline + error.EndOfStream => {}, + else => return err, + }; + + // Parse the header JSON + const headers: std.json.ObjectMap = headers: { + const line = std.mem.trim(u8, buf.written(), " \t"); + if (line.len == 0) return null; + + const value = try std.json.parseFromSliceLeaky( + std.json.Value, + alloc, + line, + .{ .allocate = .alloc_if_needed }, + ); + + break :headers switch (value) { + .object => |map| map, + else => return error.EnvelopeItemMalformedHeaders, + }; + }; + + // Get the event type + const typ: ItemType = if (headers.get("type")) |v| switch (v) { + .string => |str| std.meta.stringToEnum( + ItemType, + str, + ) orelse .unknown, + else => return error.EnvelopeItemTypeMissing, + } else return error.EnvelopeItemTypeMissing; + + // Get the payload length. The length is not required. If the length + // is not specified then it is the next line ending in `\n`. + const len_: ?u64 = if (headers.get("length")) |v| switch (v) { + .integer => |int| std.math.cast( + u64, + int, + ) orelse return error.EnvelopeItemLengthMalformed, + else => return error.EnvelopeItemLengthMalformed, + } else null; + + // Get the payload + const payload: []const u8 = if (len_) |len| payload: { + // The payload length is specified so read the exact length. + var payload: std.Io.Writer.Allocating = .init(alloc); + defer payload.deinit(); + + reader.streamExact(&payload.writer, len) catch |err| switch (err) { + error.EndOfStream => return error.EnvelopeItemPayloadTooShort, + else => return err, + }; + + // The next byte must be a newline. + if (reader.takeByte()) |byte| { + if (byte != '\n') return error.EnvelopeItemPayloadNoNewline; + } else |err| switch (err) { + error.EndOfStream => {}, + else => return err, + } + + break :payload try payload.toOwnedSlice(); + } else payload: { + // The payload is the next line ending in `\n`. It is required. + var payload: std.Io.Writer.Allocating = .init(alloc); + _ = reader.streamDelimiterLimit( + &payload.writer, + '\n', + .limited(1024 * 1024), // 50MB, arbitrary choice + ) catch |err| switch (err) { + error.StreamTooLong => return error.EnvelopeItemPayloadTooShort, + else => |v| return v, + }; + _ = reader.discardDelimiterInclusive('\n') catch |err| switch (err) { + // It's okay if there isn't a trailing newline + error.EndOfStream => {}, + else => return err, + }; + break :payload try payload.toOwnedSlice(); + }; + + return .{ .encoded = .{ + .headers = headers, + .type = typ, + .payload = payload, + } }; + } + + pub fn deinit(self: *Envelope) void { + self.arena.deinit(); + } + + /// The arena allocator associated with this envelope + pub fn allocator(self: *Envelope) Allocator { + return self.arena.allocator(); + } + + /// Serialize the envelope to the given writer. + /// + /// This will convert all decoded items to encoded items and + /// therefore may allocate. + pub fn serialize( + self: *Envelope, + writer: *std.Io.Writer, + ) !void { + // Header line first + try writer.print("{f}\n", .{std.json.fmt( + std.json.Value{ .object = self.headers }, + json_opts, + )}); + + // Write each item + const alloc = self.allocator(); + for (self.items.items, 0..) |*item, idx| { + if (idx > 0) try writer.writeByte('\n'); + + const encoded = try item.encode(alloc); + assert(item.* == .encoded); + + try writer.print("{f}\n{s}", .{ + std.json.fmt( + std.json.Value{ .object = encoded.headers }, + json_opts, + ), + encoded.payload, + }); + } + } +}; + +/// The various item types that can be in an envelope. This is a point +/// in time snapshot of the types that are known whenever this is edited. +/// Event types can be introduced at any time and unknown types will +/// take the "unknown" enum value. +/// +/// https://develop.sentry.dev/sdk/envelopes/#data-model +pub const ItemType = enum { + /// Special event type for when the item type is unknown. + unknown, + + /// Documented event types + event, + transaction, + attachment, + session, + sessions, + statsd, + metric_meta, + user_feedback, + client_report, + replay_event, + replay_recording, + profile, + check_in, +}; + +/// An item in the envelope. An item can be either in an encoded +/// or decoded state. The encoded state lets us parse the envelope +/// more cheaply since we can defer the full decoding of the item +/// until we need it. +/// +/// The decoded state is more ergonomic to work with and lets us +/// easily build up new items and defer encoding until serialization +/// time. +pub const Item = union(enum) { + encoded: EncodedItem, + attachment: Attachment, + + /// Convert the item to an encoded item. This modify the item + /// in place. + pub fn encode( + self: *Item, + alloc: Allocator, + ) !EncodedItem { + const result: EncodedItem = switch (self.*) { + .encoded => |v| return v, + .attachment => |*v| try v.encode(alloc), + }; + self.* = .{ .encoded = result }; + return result; + } + + /// Returns the type of item represented here, whether + /// it is an encoded item or not. + pub fn itemType(self: Item) ItemType { + return switch (self) { + .encoded => |v| v.type, + .attachment => .attachment, + }; + } + + pub const DecodeError = Allocator.Error || error{ + MissingRequiredField, + InvalidFieldType, + UnsupportedType, + }; + + /// Decode the item if it is encoded. This will modify itself. + /// If the item is already decoded this does nothing. + /// + /// The allocator argument should be an arena-style allocator, + /// typically the allocator associated with the Envelope. + /// + /// If the decoding fails because the item is in an invalid + /// state (i.e. its missing a required field) then this will + /// error but the encoded item will remain unmodified. This + /// allows the caller to handle the error without corrupting the + /// envelope. + /// + /// If decoding fails, the allocator may still allocate so the + /// allocator should be an arena-style allocator. + pub fn decode(self: *Item, alloc: Allocator) DecodeError!void { + // Get our encoded item. If we're not encoded we're done. + const encoded: EncodedItem = switch (self.*) { + .encoded => |v| v, + else => return, + }; + + // Decode the item. + self.* = switch (encoded.type) { + .attachment => .{ .attachment = try .decode( + alloc, + encoded, + ) }, + else => return error.UnsupportedType, + }; + } +}; + +/// An encoded item. It is "encoded" in the sense that the payload +/// is a byte slice. The headers are "decoded" into a json ObjectMap +/// but that's still a pretty low-level representation. +pub const EncodedItem = struct { + headers: std.json.ObjectMap, + type: ItemType, + payload: []const u8, +}; + +/// An arbitrary file attachment. +/// +/// https://develop.sentry.dev/sdk/envelopes/#attachment +pub const Attachment = struct { + /// "filename" field is the name of the uploaded file without + /// a path component. + filename: []const u8, + + /// A special "type" associated with the attachment. This + /// is documented on the Sentry website. In the future we should + /// make this an enum. + type: ?[]const u8 = null, + + /// Additional headers for the attachment. + headers_extra: ObjectMapUnmanaged = .{}, + + /// The data for the attachment. + payload: []const u8, + + pub fn decode( + alloc: Allocator, + item: EncodedItem, + ) Item.DecodeError!Attachment { + _ = alloc; + + return .{ + .filename = if (item.headers.get("filename")) |v| switch (v) { + .string => |str| str, + else => return error.InvalidFieldType, + } else return error.MissingRequiredField, + + .type = if (item.headers.get("attachment_type")) |v| switch (v) { + .string => |str| str, + else => return error.InvalidFieldType, + } else null, + + .headers_extra = item.headers.unmanaged, + .payload = item.payload, + }; + } + + pub fn encode( + self: *Attachment, + alloc: Allocator, + ) !EncodedItem { + try self.headers_extra.put( + alloc, + "filename", + .{ .string = self.filename }, + ); + + if (self.type) |v| { + try self.headers_extra.put( + alloc, + "attachment_type", + .{ .string = v }, + ); + } else { + _ = self.headers_extra.swapRemove("attachment_type"); + } + + return .{ + .headers = self.headers_extra.promote(alloc), + .type = .attachment, + .payload = self.payload, + }; + } +}; + +/// Same as std.json.ObjectMap but unmanaged. This lets us store +/// them alongside all our items without the overhead of duplicated +/// allocators. Additional, items do not own their own memory so this +/// makes it clear that deinit of an item will not free the memory. +pub const ObjectMapUnmanaged = std.StringArrayHashMapUnmanaged(std.json.Value); + +/// The options we must use for serialization. +const json_opts: std.json.Stringify.Options = .{ + // This is the default but I want to be explicit because its + // VERY important for the correctness of the envelope. This is + // the only whitespace type in std.json that doesn't emit newlines. + // All JSON headers in the envelope must be on a single line. + .whitespace = .minified, +}; + +test "Envelope parse" { + const testing = std.testing; + const alloc = testing.allocator; + + var reader: std.Io.Reader = .fixed( + \\{} + ); + var v = try Envelope.parse(alloc, &reader); + defer v.deinit(); +} + +test "Envelope parse session" { + const testing = std.testing; + const alloc = testing.allocator; + + var reader: std.Io.Reader = .fixed( + \\{} + \\{"type":"session","length":218} + \\{"init":true,"sid":"c148cc2f-5f9f-4231-575c-2e85504d6434","status":"abnormal","errors":0,"started":"2024-08-29T02:38:57.607016Z","duration":0.000343,"attrs":{"release":"0.1.0-HEAD+d37b7d09","environment":"production"}} + ); + var v = try Envelope.parse(alloc, &reader); + defer v.deinit(); + + try testing.expectEqual(@as(usize, 1), v.items.items.len); + try testing.expectEqual(ItemType.session, v.items.items[0].encoded.type); +} + +test "Envelope parse multiple" { + const testing = std.testing; + const alloc = testing.allocator; + + var reader: std.Io.Reader = .fixed( + \\{} + \\{"type":"session","length":218} + \\{"init":true,"sid":"c148cc2f-5f9f-4231-575c-2e85504d6434","status":"abnormal","errors":0,"started":"2024-08-29T02:38:57.607016Z","duration":0.000343,"attrs":{"release":"0.1.0-HEAD+d37b7d09","environment":"production"}} + \\{"type":"attachment","length":4,"filename":"test.txt"} + \\ABCD + ); + var v = try Envelope.parse(alloc, &reader); + defer v.deinit(); + + try testing.expectEqual(@as(usize, 2), v.items.items.len); + try testing.expectEqual(ItemType.session, v.items.items[0].encoded.type); + try testing.expectEqual(ItemType.attachment, v.items.items[1].encoded.type); +} + +test "Envelope parse multiple no length" { + const testing = std.testing; + const alloc = testing.allocator; + + var reader: std.Io.Reader = .fixed( + \\{} + \\{"type":"session"} + \\{} + \\{"type":"attachment","length":4,"filename":"test.txt"} + \\ABCD + ); + var v = try Envelope.parse(alloc, &reader); + defer v.deinit(); + + try testing.expectEqual(@as(usize, 2), v.items.items.len); + try testing.expectEqual(ItemType.session, v.items.items[0].encoded.type); + try testing.expectEqual(ItemType.attachment, v.items.items[1].encoded.type); +} + +test "Envelope parse end in new line" { + const testing = std.testing; + const alloc = testing.allocator; + + var reader: std.Io.Reader = .fixed( + \\{} + \\{"type":"session","length":218} + \\{"init":true,"sid":"c148cc2f-5f9f-4231-575c-2e85504d6434","status":"abnormal","errors":0,"started":"2024-08-29T02:38:57.607016Z","duration":0.000343,"attrs":{"release":"0.1.0-HEAD+d37b7d09","environment":"production"}} + \\ + ); + var v = try Envelope.parse(alloc, &reader); + defer v.deinit(); + + try testing.expectEqual(@as(usize, 1), v.items.items.len); + try testing.expectEqual(ItemType.session, v.items.items[0].encoded.type); +} + +test "Envelope parse attachment" { + const testing = std.testing; + const alloc = testing.allocator; + + var reader: std.Io.Reader = .fixed( + \\{} + \\{"type":"attachment","length":4,"filename":"test.txt"} + \\ABCD + ); + var v = try Envelope.parse(alloc, &reader); + defer v.deinit(); + + try testing.expectEqual(@as(usize, 1), v.items.items.len); + + var item = &v.items.items[0]; + try testing.expectEqual(ItemType.attachment, item.encoded.type); + try item.decode(v.allocator()); + try testing.expect(item.* == .attachment); + try testing.expectEqualStrings("test.txt", item.attachment.filename); + + // Serialization test + { + var output: std.Io.Writer.Allocating = .init(alloc); + defer output.deinit(); + try v.serialize(&output.writer); + try testing.expectEqualStrings( + \\{} + \\{"type":"attachment","length":4,"filename":"test.txt"} + \\ABCD + , std.mem.trim(u8, output.written(), "\n")); + } +} + +test "Envelope serialize empty" { + const testing = std.testing; + const alloc = testing.allocator; + + var reader: std.Io.Reader = .fixed( + \\{} + ); + var v = try Envelope.parse(alloc, &reader); + defer v.deinit(); + + var output: std.Io.Writer.Allocating = .init(alloc); + defer output.deinit(); + try v.serialize(&output.writer); + + try testing.expectEqualStrings( + \\{} + , std.mem.trim(u8, output.written(), "\n")); +} + +test "Envelope serialize session" { + const testing = std.testing; + const alloc = testing.allocator; + + var reader: std.Io.Reader = .fixed( + \\{} + \\{"type":"session","length":218} + \\{"init":true,"sid":"c148cc2f-5f9f-4231-575c-2e85504d6434","status":"abnormal","errors":0,"started":"2024-08-29T02:38:57.607016Z","duration":0.000343,"attrs":{"release":"0.1.0-HEAD+d37b7d09","environment":"production"}} + ); + var v = try Envelope.parse(alloc, &reader); + defer v.deinit(); + + var output: std.Io.Writer.Allocating = .init(alloc); + defer output.deinit(); + try v.serialize(&output.writer); + + try testing.expectEqualStrings( + \\{} + \\{"type":"session","length":218} + \\{"init":true,"sid":"c148cc2f-5f9f-4231-575c-2e85504d6434","status":"abnormal","errors":0,"started":"2024-08-29T02:38:57.607016Z","duration":0.000343,"attrs":{"release":"0.1.0-HEAD+d37b7d09","environment":"production"}} + , std.mem.trim(u8, output.written(), "\n")); +} |
