mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-17 09:16:11 +03:00
crash: parse attachments from sentry envelope
This commit is contained in:
@ -270,7 +270,7 @@ pub const Transport = struct {
|
|||||||
|
|
||||||
fn shouldDiscard(envelope: *const crash.Envelope) !bool {
|
fn shouldDiscard(envelope: *const crash.Envelope) !bool {
|
||||||
// If we have an event item then we're good.
|
// If we have an event item then we're good.
|
||||||
for (envelope.items) |item| {
|
for (envelope.items.items) |item| {
|
||||||
if (item.itemType() == .event) return false;
|
if (item.itemType() == .event) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,8 @@ const std = @import("std");
|
|||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const log = std.log.scoped(.sentry_envelope);
|
||||||
|
|
||||||
/// The Sentry Envelope format: https://develop.sentry.dev/sdk/envelopes/
|
/// The Sentry Envelope format: https://develop.sentry.dev/sdk/envelopes/
|
||||||
///
|
///
|
||||||
/// The envelope is our primary crash report format since use the Sentry
|
/// The envelope is our primary crash report format since use the Sentry
|
||||||
@ -24,7 +26,7 @@ pub const Envelope = struct {
|
|||||||
headers: std.json.ObjectMap,
|
headers: std.json.ObjectMap,
|
||||||
|
|
||||||
/// The items in the envelope in the order they're encoded.
|
/// The items in the envelope in the order they're encoded.
|
||||||
items: []const Item,
|
items: std.ArrayListUnmanaged(Item),
|
||||||
|
|
||||||
/// Parse an envelope from a reader.
|
/// Parse an envelope from a reader.
|
||||||
///
|
///
|
||||||
@ -89,11 +91,14 @@ pub const Envelope = struct {
|
|||||||
fn parseItems(
|
fn parseItems(
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
reader: anytype,
|
reader: anytype,
|
||||||
) ![]const Item {
|
) !std.ArrayListUnmanaged(Item) {
|
||||||
var items = std.ArrayList(Item).init(alloc);
|
var items: std.ArrayListUnmanaged(Item) = .{};
|
||||||
defer items.deinit();
|
errdefer items.deinit(alloc);
|
||||||
while (try parseOneItem(alloc, reader)) |item| try items.append(item);
|
while (try parseOneItem(alloc, reader)) |item| {
|
||||||
return try items.toOwnedSlice();
|
try items.append(alloc, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parseOneItem(
|
fn parseOneItem(
|
||||||
@ -187,16 +192,42 @@ pub const Envelope = struct {
|
|||||||
self.arena.deinit();
|
self.arena.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encode the envelope to the given writer.
|
/// The arena allocator associated with this envelope
|
||||||
pub fn encode(self: *const Envelope, writer: anytype) !void {
|
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: anytype,
|
||||||
|
) !void {
|
||||||
// Header line first
|
// Header line first
|
||||||
try std.json.stringify(std.json.Value{ .object = self.headers }, json_opts, writer);
|
try std.json.stringify(
|
||||||
|
std.json.Value{ .object = self.headers },
|
||||||
|
json_opts,
|
||||||
|
writer,
|
||||||
|
);
|
||||||
try writer.writeByte('\n');
|
try writer.writeByte('\n');
|
||||||
|
|
||||||
// Write each item
|
// Write each item
|
||||||
for (self.items, 0..) |*item, idx| {
|
const alloc = self.allocator();
|
||||||
|
for (self.items.items, 0..) |*item, idx| {
|
||||||
if (idx > 0) try writer.writeByte('\n');
|
if (idx > 0) try writer.writeByte('\n');
|
||||||
try item.encode(writer);
|
|
||||||
|
const encoded = try item.encode(alloc);
|
||||||
|
assert(item.* == .encoded);
|
||||||
|
|
||||||
|
try std.json.stringify(
|
||||||
|
std.json.Value{ .object = encoded.headers },
|
||||||
|
json_opts,
|
||||||
|
writer,
|
||||||
|
);
|
||||||
|
try writer.writeByte('\n');
|
||||||
|
try writer.writeAll(encoded.payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -239,15 +270,18 @@ pub const Item = union(enum) {
|
|||||||
encoded: EncodedItem,
|
encoded: EncodedItem,
|
||||||
attachment: Attachment,
|
attachment: Attachment,
|
||||||
|
|
||||||
|
/// Convert the item to an encoded item. This modify the item
|
||||||
|
/// in place.
|
||||||
pub fn encode(
|
pub fn encode(
|
||||||
self: Item,
|
self: *Item,
|
||||||
writer: anytype,
|
alloc: Allocator,
|
||||||
) !void {
|
) !EncodedItem {
|
||||||
switch (self) {
|
const result: EncodedItem = switch (self.*) {
|
||||||
inline .encoded,
|
.encoded => |v| return v,
|
||||||
.attachment,
|
.attachment => |*v| try v.encode(alloc),
|
||||||
=> |v| try v.encode(writer),
|
};
|
||||||
}
|
self.* = .{ .encoded = result };
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the type of item represented here, whether
|
/// Returns the type of item represented here, whether
|
||||||
@ -258,6 +292,43 @@ pub const Item = union(enum) {
|
|||||||
.attachment => .attachment,
|
.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 Attachment.decode(
|
||||||
|
alloc,
|
||||||
|
encoded,
|
||||||
|
) },
|
||||||
|
else => return error.UnsupportedType,
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// An encoded item. It is "encoded" in the sense that the payload
|
/// An encoded item. It is "encoded" in the sense that the payload
|
||||||
@ -267,19 +338,6 @@ pub const EncodedItem = struct {
|
|||||||
headers: std.json.ObjectMap,
|
headers: std.json.ObjectMap,
|
||||||
type: ItemType,
|
type: ItemType,
|
||||||
payload: []const u8,
|
payload: []const u8,
|
||||||
|
|
||||||
pub fn encode(
|
|
||||||
self: EncodedItem,
|
|
||||||
writer: anytype,
|
|
||||||
) !void {
|
|
||||||
try std.json.stringify(
|
|
||||||
std.json.Value{ .object = self.headers },
|
|
||||||
json_opts,
|
|
||||||
writer,
|
|
||||||
);
|
|
||||||
try writer.writeByte('\n');
|
|
||||||
try writer.writeAll(self.payload);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// An arbitrary file attachment.
|
/// An arbitrary file attachment.
|
||||||
@ -296,18 +354,58 @@ pub const Attachment = struct {
|
|||||||
type: ?[]const u8 = null,
|
type: ?[]const u8 = null,
|
||||||
|
|
||||||
/// Additional headers for the attachment.
|
/// Additional headers for the attachment.
|
||||||
header_extra: ObjectMapUnmanaged = .{},
|
headers_extra: ObjectMapUnmanaged = .{},
|
||||||
|
|
||||||
/// The data for the attachment.
|
/// The data for the attachment.
|
||||||
payload: []const u8,
|
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(
|
pub fn encode(
|
||||||
self: Attachment,
|
self: *Attachment,
|
||||||
writer: anytype,
|
alloc: Allocator,
|
||||||
) !void {
|
) !EncodedItem {
|
||||||
_ = self;
|
try self.headers_extra.put(
|
||||||
_ = writer;
|
alloc,
|
||||||
@panic("TODO");
|
"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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -349,8 +447,8 @@ test "Envelope parse session" {
|
|||||||
var v = try Envelope.parse(alloc, fbs.reader());
|
var v = try Envelope.parse(alloc, fbs.reader());
|
||||||
defer v.deinit();
|
defer v.deinit();
|
||||||
|
|
||||||
try testing.expectEqual(@as(usize, 1), v.items.len);
|
try testing.expectEqual(@as(usize, 1), v.items.items.len);
|
||||||
try testing.expectEqual(ItemType.session, v.items[0].encoded.type);
|
try testing.expectEqual(ItemType.session, v.items.items[0].encoded.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Envelope parse end in new line" {
|
test "Envelope parse end in new line" {
|
||||||
@ -366,11 +464,44 @@ test "Envelope parse end in new line" {
|
|||||||
var v = try Envelope.parse(alloc, fbs.reader());
|
var v = try Envelope.parse(alloc, fbs.reader());
|
||||||
defer v.deinit();
|
defer v.deinit();
|
||||||
|
|
||||||
try testing.expectEqual(@as(usize, 1), v.items.len);
|
try testing.expectEqual(@as(usize, 1), v.items.items.len);
|
||||||
try testing.expectEqual(ItemType.session, v.items[0].encoded.type);
|
try testing.expectEqual(ItemType.session, v.items.items[0].encoded.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Envelope encode empty" {
|
test "Envelope parse attachment" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var fbs = std.io.fixedBufferStream(
|
||||||
|
\\{}
|
||||||
|
\\{"type":"attachment","length":4,"filename":"test.txt"}
|
||||||
|
\\ABCD
|
||||||
|
);
|
||||||
|
var v = try Envelope.parse(alloc, fbs.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.ArrayList(u8).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.items, "\n"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Envelope serialize empty" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
@ -382,14 +513,14 @@ test "Envelope encode empty" {
|
|||||||
|
|
||||||
var output = std.ArrayList(u8).init(alloc);
|
var output = std.ArrayList(u8).init(alloc);
|
||||||
defer output.deinit();
|
defer output.deinit();
|
||||||
try v.encode(output.writer());
|
try v.serialize(output.writer());
|
||||||
|
|
||||||
try testing.expectEqualStrings(
|
try testing.expectEqualStrings(
|
||||||
\\{}
|
\\{}
|
||||||
, std.mem.trim(u8, output.items, "\n"));
|
, std.mem.trim(u8, output.items, "\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Envelope encode session" {
|
test "Envelope serialize session" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
@ -403,7 +534,7 @@ test "Envelope encode session" {
|
|||||||
|
|
||||||
var output = std.ArrayList(u8).init(alloc);
|
var output = std.ArrayList(u8).init(alloc);
|
||||||
defer output.deinit();
|
defer output.deinit();
|
||||||
try v.encode(output.writer());
|
try v.serialize(output.writer());
|
||||||
|
|
||||||
try testing.expectEqualStrings(
|
try testing.expectEqualStrings(
|
||||||
\\{}
|
\\{}
|
||||||
|
Reference in New Issue
Block a user